diff --git a/cmd/root.go b/cmd/root.go index 9abe5aa5068a9ee15dedc5beb0e8223073f420ba..2c4c776a2acf325058ee9725bad3491ff26ab209 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -11,6 +11,7 @@ package cmd import ( "fmt" "github.com/mitchellh/go-homedir" + "github.com/pkg/errors" "github.com/spf13/cobra" jww "github.com/spf13/jwalterweatherman" "github.com/spf13/viper" @@ -18,8 +19,11 @@ import ( "gitlab.com/elixxir/notifications-bot/notifications" "gitlab.com/elixxir/notifications-bot/storage" "gitlab.com/elixxir/primitives/id" + "gitlab.com/elixxir/primitives/ndf" + "gitlab.com/elixxir/primitives/utils" "os" "path" + "strings" ) var ( @@ -53,19 +57,20 @@ var rootCmd = &cobra.Command{ certPath := viper.GetString("certPath") keyPath := viper.GetString("keyPath") localAddress := fmt.Sprintf("0.0.0.0:%d", viper.GetInt("port")) + fbCreds := viper.GetString("firebaseCredentialsPath") // Populate params NotificationParams = notifications.Params{ Address: localAddress, CertPath: certPath, KeyPath: keyPath, + FBCreds: fbCreds, } jww.INFO.Println("Starting Notifications...") - impl, err := notifications.StartNotifications(NotificationParams, noTLS) + impl, err := notifications.StartNotifications(NotificationParams, noTLS, false) if err != nil { - err = fmt.Errorf("Failed to start notifications server: %+v", err) - panic(err) + jww.FATAL.Panicf("Failed to start notifications server: %+v", err) } impl.Storage = storage.NewDatabase( @@ -75,25 +80,54 @@ var rootCmd = &cobra.Command{ viper.GetString("dbAddress"), ) - permissioningAddr := viper.GetString("permissioningAddress") - permissioningCertPath := viper.GetString("permissioningCertPath") - _, err = impl.Comms.AddHost(id.PERMISSIONING, permissioningAddr, []byte(permissioningCertPath), true, true) - - if err != nil{ - jww.FATAL.Panicf("Failed to Create permissioning host: %+v", err) + err = setupConnection(impl, viper.GetString("permissioningCertPath"), viper.GetString("permissioningAddress")) + if err != nil { + jww.FATAL.Panicf("Failed to set up connections: %+v", err) } // Start notification loop killChan := make(chan struct{}) - go impl.RunNotificationLoop(viper.GetString("firebaseCredentialsPath"), - loopDelay, - killChan) + errChan := make(chan error) + go impl.RunNotificationLoop(loopDelay, killChan, errChan) // Wait forever to prevent process from ending - select {} + err = <-errChan + panic(err) }, } +// setupConnection handles connecting to permissioning and polling for the NDF once connected +func setupConnection(impl *notifications.Impl, permissioningCertPath, permissioningAddr string) error { + // Read in permissioning certificate + cert, err := utils.ReadFile(permissioningCertPath) + if err != nil { + return errors.Wrap(err, "Could not read permissioning cert") + } + + // Add host for permissioning server + _, err = impl.Comms.AddHost(id.PERMISSIONING, permissioningAddr, cert, true, false) + if err != nil { + return errors.Wrap(err, "Failed to Create permissioning host") + } + + // Loop until an NDF is received + var def *ndf.NetworkDefinition + for def == nil { + def, err = notifications.PollNdf(nil, impl.Comms) + // Don't stop if error is expected + if err != nil && !strings.Contains(err.Error(), ndf.NO_NDF) { + return errors.Wrap(err, "Failed to get NDF") + } + } + + // Update NDF & gateway host + err = impl.UpdateNdf(def) + if err != nil { + return errors.Wrap(err, "Failed to update impl's NDF") + } + return nil +} + // Execute adds all child commands to the root command and sets flags // appropriately. This is called by main.main(). It only needs to // happen once to the rootCmd. @@ -125,8 +159,8 @@ func init() { rootCmd.Flags().BoolVar(&noTLS, "noTLS", false, "Runs without TLS enabled") - rootCmd.Flags().IntVarP(&loopDelay, "loopDelay", "", 5, - "Set the delay between notification loops (in seconds)") + rootCmd.Flags().IntVarP(&loopDelay, "loopDelay", "", 500, + "Set the delay between notification loops (in milliseconds)") // Bind config and command line flags of the same name err := viper.BindPFlag("verbose", rootCmd.Flags().Lookup("verbose")) diff --git a/firebase/fcm.go b/firebase/fcm.go index 5a05e3f47ddcf23b12cddfb7065b82416b05208e..665e50ac06e2ac57ba08937343f4d5d525b249fd 100644 --- a/firebase/fcm.go +++ b/firebase/fcm.go @@ -19,13 +19,12 @@ import ( // function types for use in notificationsbot struct type SetupFunc func(string) (*messaging.Client, context.Context, error) -type SendFunc func(FBSender, context.Context, string) (string, error) +type SendFunc func(FBSender, string) (string, error) // FirebaseComm is a struct which holds the functions to setup the messaging app and sending notifications // Using a struct in this manner allows us to properly unit test the NotifyUser function type FirebaseComm struct { - SetupMessagingApp SetupFunc - SendNotification SendFunc + SendNotification SendFunc } // This interface matches the send function in the messaging app, allowing us to unit test sendNotification @@ -36,44 +35,43 @@ type FBSender interface { // Set up a notificationbot object with the proper setup and send functions func NewFirebaseComm() *FirebaseComm { return &FirebaseComm{ - SetupMessagingApp: setupMessagingApp, - SendNotification: sendNotification, + SendNotification: sendNotification, } } // FOR TESTING USE ONLY: setup a notificationbot object with mocked setup and send funcs -func NewMockFirebaseComm(t *testing.T, setupFunc SetupFunc, sendFunc SendFunc) *FirebaseComm { +func NewMockFirebaseComm(t *testing.T, sendFunc SendFunc) *FirebaseComm { if t == nil { panic("This method should only be used in tests") } return &FirebaseComm{ - SetupMessagingApp: setupFunc, - SendNotification: sendFunc, + SendNotification: sendFunc, } } // setupApp is a helper function which sets up a connection with firebase // It returns a messaging client, a context object and an error -func setupMessagingApp(serviceKeyPath string) (*messaging.Client, context.Context, error) { +func SetupMessagingApp(serviceKeyPath string) (*messaging.Client, error) { ctx := context.Background() opt := option.WithCredentialsFile(serviceKeyPath) app, err := firebase.NewApp(context.Background(), nil, opt) if err != nil { - return nil, nil, errors.Errorf("Error initializing app: %v", err) + return nil, errors.Errorf("Error initializing app: %v", err) } client, err := app.Messaging(ctx) if err != nil { - return nil, nil, errors.Errorf("Error getting Messaging app: %+v", err) + return nil, errors.Errorf("Error getting Messaging app: %+v", err) } - return client, ctx, nil + return client, nil } // SendNotification accepts a registration token and service account file // It gets the proper infrastructure, then builds & sends a notification through the firebase admin API // returns string, error (string is of dubious use, but is returned for the time being) -func sendNotification(app FBSender, ctx context.Context, token string) (string, error) { +func sendNotification(app FBSender, token string) (string, error) { + ctx := context.Background() message := &messaging.Message{ Notification: &messaging.Notification{ Title: "xx Messenger", @@ -84,7 +82,7 @@ func sendNotification(app FBSender, ctx context.Context, token string) (string, resp, err := app.Send(ctx, message) if err != nil { - return "", errors.Errorf("Failed to send notification: %+v", err) + return "", errors.Wrap(err, "Failed to send notification") } return resp, nil } diff --git a/firebase/fcm_test.go b/firebase/fcm_test.go index c5943626fccb162d1cf34ef5811cee1578a244c3..dc9ec369d4341e10d5bb3c9f459c5f891621b115 100644 --- a/firebase/fcm_test.go +++ b/firebase/fcm_test.go @@ -22,10 +22,9 @@ func (MockSender) Send(ctx context.Context, app *messaging.Message) (string, err // This tests the function which sends a notification to firebase. // Note: this requires you to have a valid token & service credentials func TestSendNotification(t *testing.T) { - ctx := context.Background() app := MockSender{} - _, err := sendNotification(app, ctx, token) + _, err := sendNotification(app, token) if err != nil { t.Error(err.Error()) } @@ -34,7 +33,7 @@ func TestSendNotification(t *testing.T) { // Unit test the NewFirebaseComm method func TestNewFirebaseComm(t *testing.T) { comm := NewFirebaseComm() - if comm.SendNotification == nil || comm.SetupMessagingApp == nil { + if comm.SendNotification == nil { t.Error("Failed to set functions in comm") } } diff --git a/go.mod b/go.mod index e7f4343db20f6bea7162c91f21ea53081070a7bd..6b515575129952614594e5a88c325eee8df5b508 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( cloud.google.com/go/pubsub v1.2.0 // indirect firebase.google.com/go v3.12.0+incompatible github.com/go-pg/pg v8.0.6+incompatible - github.com/gopherjs/gopherjs v0.0.0-20191106031601-ce3c9ade29de // indirect + github.com/gopherjs/gopherjs v0.0.0-20200209183636-89e6cbcd0b6d // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/mitchellh/go-homedir v1.1.0 github.com/onsi/ginkgo v1.12.0 // indirect @@ -22,15 +22,15 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.6.2 - gitlab.com/elixxir/comms v0.0.0-20200207001605-3b6328b48ed1 + gitlab.com/elixxir/comms v0.0.0-20200210222003-b4b1712bc2f4 gitlab.com/elixxir/crypto v0.0.0-20200206203107-b8926242da23 - gitlab.com/elixxir/primitives v0.0.0-20200207225613-9a4445ddec16 - golang.org/x/crypto v0.0.0-20200207205829-a95e85b341fd // indirect + gitlab.com/elixxir/primitives v0.0.0-20200210205543-5c55c1f6949f + golang.org/x/crypto v0.0.0-20200210222208-86ce3cb69678 // indirect golang.org/x/exp v0.0.0-20200207192155-f17229e696bd // indirect golang.org/x/net v0.0.0-20200202094626-16171245cfb2 - golang.org/x/tools v0.0.0-20200207224406-61798d64f025 // indirect + golang.org/x/tools v0.0.0-20200211180503-f41547ceafb9 // indirect google.golang.org/api v0.17.0 - google.golang.org/genproto v0.0.0-20200207204624-4f3edf09f4f6 // indirect + google.golang.org/genproto v0.0.0-20200211111953-2dc5924e3898 // indirect gopkg.in/ini.v1 v1.52.0 // indirect mellium.im/sasl v0.0.0-20190815210834-e27ea4901008 // indirect ) diff --git a/go.sum b/go.sum index 8462895df475c24855d1834855494d168f899078..9d755199319f779578c35e0c97f1f3c0f1988bd6 100644 --- a/go.sum +++ b/go.sum @@ -109,8 +109,8 @@ github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gopherjs/gopherjs v0.0.0-20191106031601-ce3c9ade29de h1:F7WD09S8QB4LrkEpka0dFPLSotH11HRpCsLIbIcJ7sU= -github.com/gopherjs/gopherjs v0.0.0-20191106031601-ce3c9ade29de/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20200209183636-89e6cbcd0b6d h1:vr95xIx8Eg3vCzZPxY3rCwTfkjqNDt/FgVqTOk0WByk= +github.com/gopherjs/gopherjs v0.0.0-20200209183636-89e6cbcd0b6d/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 h1:Iju5GlWwrvL6UBg4zJJt3btmonfrMlCDdsejg4CZE7c= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= @@ -224,8 +224,8 @@ github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGr github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -gitlab.com/elixxir/comms v0.0.0-20200207001605-3b6328b48ed1 h1:tsDj1q0L2aGIfxdNSytui2agGfnLo7Y3C7hgSRmZofA= -gitlab.com/elixxir/comms v0.0.0-20200207001605-3b6328b48ed1/go.mod h1:maK5To73lT8L8ygCoNsFUj+b1gRfYk45w8IxiO6Y6HA= +gitlab.com/elixxir/comms v0.0.0-20200210222003-b4b1712bc2f4 h1:3alfwf6IVMv749DYMhUGg171MLjQeALXYkvHfyarSUI= +gitlab.com/elixxir/comms v0.0.0-20200210222003-b4b1712bc2f4/go.mod h1:maK5To73lT8L8ygCoNsFUj+b1gRfYk45w8IxiO6Y6HA= gitlab.com/elixxir/crypto v0.0.0-20200108005412-8159c60663f9 h1:MJ87g3yMIvA9MTDMojuqaw1tCxU6LIKzxMaH3oEiP4M= gitlab.com/elixxir/crypto v0.0.0-20200108005412-8159c60663f9/go.mod h1:+46Zj/NE6JEkXExYnzdvvDokPpDbA+fJsRszvrezK9k= gitlab.com/elixxir/crypto v0.0.0-20200206203107-b8926242da23 h1:J9MKdOxLGzDZoLy2Q0CAxPlPjSH+k4NG3JhgvatAZjo= @@ -234,8 +234,8 @@ gitlab.com/elixxir/primitives v0.0.0-20191028233752-882c08b8f095 h1:fnRh0PUwgy0q gitlab.com/elixxir/primitives v0.0.0-20191028233752-882c08b8f095/go.mod h1:+UiRRWzNpl/WoWUuQtJSoimfXImJAJ5lrrmg0pQKY3g= gitlab.com/elixxir/primitives v0.0.0-20200131183153-e93c6b75019f h1:F0YwFZz4umoXOJ+xX34WIRrucuLgHCSyKxWOKGaEt5g= gitlab.com/elixxir/primitives v0.0.0-20200131183153-e93c6b75019f/go.mod h1:REJMcwIcyxh74VSHqy4S9yYiaEsQYObOPglRExDpk14= -gitlab.com/elixxir/primitives v0.0.0-20200207225613-9a4445ddec16 h1:ifJ/7gl7odnp8iz09ranziiSmH+ZI4CalQW2PQn0W6M= -gitlab.com/elixxir/primitives v0.0.0-20200207225613-9a4445ddec16/go.mod h1:REJMcwIcyxh74VSHqy4S9yYiaEsQYObOPglRExDpk14= +gitlab.com/elixxir/primitives v0.0.0-20200210205543-5c55c1f6949f h1:1bYEOz3/a3Q09dxE0ND64+9l21MsyeailzoFYEGC93s= +gitlab.com/elixxir/primitives v0.0.0-20200210205543-5c55c1f6949f/go.mod h1:REJMcwIcyxh74VSHqy4S9yYiaEsQYObOPglRExDpk14= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -261,8 +261,8 @@ golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72 h1:+ELyKg6m8UBf0nPFSqD0mi golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200206161412-a0c6ece9d31a h1:aczoJ0HPNE92XKa7DrIzkNN6esOKO2TBwiiYoKcINhA= golang.org/x/crypto v0.0.0-20200206161412-a0c6ece9d31a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200207205829-a95e85b341fd h1:Wf+k80tMDcKkeQiGIgNFmhv4GDSTXoUBhm+33x2ApdA= -golang.org/x/crypto v0.0.0-20200207205829-a95e85b341fd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200210222208-86ce3cb69678 h1:wCWoJcFExDgyYx2m2hpHgwz8W3+FPdfldvIgzqDIhyg= +golang.org/x/crypto v0.0.0-20200210222208-86ce3cb69678/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -271,6 +271,7 @@ golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299 h1:zQpM52jfKHG6II1ISZY1ZcpygvuSFZpLwfluuF89XOg= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a h1:7Wlg8L54In96HTWOaI4sreLJ6qfyGuvSau5el3fK41Y= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd h1:zkO/Lhoka23X63N9OSzpSeROEUQ5ODw47tM3YWjygbs= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= @@ -391,8 +392,8 @@ golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207224406-61798d64f025 h1:i84/3szN87uN9jFX/jRqUbszQto2oAsFlqPf6lbR8H4= -golang.org/x/tools v0.0.0-20200207224406-61798d64f025/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200211180503-f41547ceafb9 h1:KeJ3+w5uHav84XuoI3aaoBb/zOzHwONuoT8/YaxGf+0= +golang.org/x/tools v0.0.0-20200211180503-f41547ceafb9/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= @@ -433,8 +434,8 @@ google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90 h1:7THRSvPuzF1bql5 google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200205142000-a86caf926a67 h1:MBO9fkVSrTpJ8vgHLPi5gb+ZWXEy7/auJN8yqyu9EiE= google.golang.org/genproto v0.0.0-20200205142000-a86caf926a67/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200207204624-4f3edf09f4f6 h1:tirixpud1WdjE3/NrL9ar4ot0ADfwls8sOcIf1ivRDw= -google.golang.org/genproto v0.0.0-20200207204624-4f3edf09f4f6/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200211111953-2dc5924e3898 h1:bKX1IGaSj2XD9yfWNts9HKRdQRH0lOZ0S7Nb8meQSlY= +google.golang.org/genproto v0.0.0-20200211111953-2dc5924e3898/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0= diff --git a/notifications/notifications.go b/notifications/notifications.go index b0c605164d294b7ef24ade98cdec4a0225e2814e..44abda7737b839b69ae8923e3e7aa638d74da4b2 100644 --- a/notifications/notifications.go +++ b/notifications/notifications.go @@ -10,6 +10,7 @@ package notifications import ( "crypto/x509" + "firebase.google.com/go/messaging" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/comms/connect" @@ -20,14 +21,14 @@ import ( "gitlab.com/elixxir/notifications-bot/firebase" "gitlab.com/elixxir/notifications-bot/storage" "gitlab.com/elixxir/primitives/id" - ndf "gitlab.com/elixxir/primitives/ndf" + "gitlab.com/elixxir/primitives/ndf" "gitlab.com/elixxir/primitives/utils" "time" ) // Function type definitions for the main operations (poll and notify) -type PollFunc func(*connect.Host, RequestInterface) ([]string, error) -type NotifyFunc func(string, string, *firebase.FirebaseComm, storage.Storage) (string, error) +type PollFunc func(*Impl) ([]string, error) +type NotifyFunc func(*messaging.Client, string, *firebase.FirebaseComm, storage.Storage) (string, error) // Params struct holds info passed in for configuration type Params struct { @@ -35,55 +36,63 @@ type Params struct { CertPath string KeyPath string PublicAddress string + FBCreds string } // Local impl for notifications; holds comms, storage object, creds and main functions type Impl struct { - Comms *notificationBot.Comms + Comms NotificationComms Storage storage.Storage notificationCert *x509.Certificate notificationKey *rsa.PrivateKey certFromFile string - gatewayHost *connect.Host // TODO: populate this field from ndf + ndf *ndf.NetworkDefinition pollFunc PollFunc notifyFunc NotifyFunc - ndf *ndf.NetworkDefinition + fcm *messaging.Client + gwId *id.Gateway } -// Request interface holds the request function from comms, allowing us to unit test polling -type RequestInterface interface { +// We use an interface here inorder to allow us to mock the getHost and RequestNDF in the notifcationsBot.Comms for testing +type NotificationComms interface { + GetHost(hostId string) (*connect.Host, bool) + AddHost(id, address string, cert []byte, disableTimeout, enableAuth bool) (host *connect.Host, err error) RequestNotifications(host *connect.Host) (*pb.IDList, error) + RequestNdf(host *connect.Host, message *pb.NDFHash) (*pb.NDF, error) } // Main function for this repo accepts credentials and an impl // loops continuously, polling for notifications and notifying the relevant users -func (nb *Impl) RunNotificationLoop(fbCreds string, loopDuration int, killChan chan struct{}) { +func (nb *Impl) RunNotificationLoop(loopDuration int, killChan chan struct{}, errChan chan error) { fc := firebase.NewFirebaseComm() for { + // Stop execution if killed by channel select { case <-killChan: return - default: + case <-time.After(time.Millisecond * time.Duration(loopDuration)): } - // TODO: fill in body of main loop, should poll gateway and send relevant notifications to firebase - UIDs, err := nb.pollFunc(nb.gatewayHost, nb.Comms) + + // Poll for UIDs to notify + UIDs, err := nb.pollFunc(nb) if err != nil { - jww.ERROR.Printf("Failed to poll gateway for users to notify: %+v", err) + errChan <- errors.Wrap(err, "Failed to poll gateway for users to notify") + return } for _, id := range UIDs { - _, err := nb.notifyFunc(id, fbCreds, fc, nb.Storage) + // Attempt to notify a given user (will not error if UID not registered) + _, err := nb.notifyFunc(nb.fcm, id, fc, nb.Storage) if err != nil { - jww.ERROR.Printf("Failed to notify user with ID %+v: %+v", id, err) + errChan <- errors.Wrapf(err, "Failed to notify user with ID %+v", id) + return } } - - time.Sleep(time.Second * time.Duration(loopDuration)) } } // StartNotifications creates an Impl from the information passed in -func StartNotifications(params Params, noTLS bool) (*Impl, error) { +func StartNotifications(params Params, noTLS, noFirebase bool) (*Impl, error) { impl := &Impl{} var cert, key []byte @@ -92,38 +101,45 @@ func StartNotifications(params Params, noTLS bool) (*Impl, error) { // Read in private key key, err = utils.ReadFile(params.KeyPath) if err != nil { - return nil, errors.Errorf("failed to read key at %+v: %+v", params.KeyPath, err) + return nil, errors.Wrapf(err, "failed to read key at %+v", params.KeyPath) } impl.notificationKey, err = rsa.LoadPrivateKeyFromPem(key) if err != nil { - return nil, errors.Errorf("Failed to parse notifications server key: %+v. "+ - "NotificationsKey is %+v", - err, impl.notificationKey) + return nil, errors.Wrapf(err, "Failed to parse notifications server key (%+v)", impl.notificationKey) } if !noTLS { // Read in TLS keys from files cert, err = utils.ReadFile(params.CertPath) if err != nil { - return nil, errors.Errorf("failed to read certificate at %+v: %+v", params.CertPath, err) + return nil, errors.Wrapf(err, "failed to read certificate at %+v", params.CertPath) } // Set globals for notification server impl.certFromFile = string(cert) impl.notificationCert, err = tls.LoadCertificate(string(cert)) if err != nil { - return nil, errors.Errorf("Failed to parse notifications server cert: %+v. "+ - "Notifications cert is %+v", - err, impl.notificationCert) + return nil, errors.Wrapf(err, "Failed to parse notifications server cert. "+ + "Notifications cert is %+v", impl.notificationCert) } } + // set up stored functions impl.pollFunc = pollForNotifications impl.notifyFunc = notifyUser + // Start notification comms server handler := NewImplementation(impl) - impl.Comms = notificationBot.StartNotificationBot(id.NOTIFICATION_BOT, params.PublicAddress, handler, cert, key) + // Set up firebase messaging client + if !noFirebase { + app, err := firebase.SetupMessagingApp(params.FBCreds) + if err != nil { + return nil, errors.Wrap(err, "Failed to setup firebase messaging app") + } + impl.fcm = app + } + return impl, nil } @@ -144,18 +160,15 @@ func NewImplementation(instance *Impl) *notificationBot.Implementation { // NotifyUser accepts a UID and service key file path. // It handles the logic involved in retrieving a user's token and sending the notification -func notifyUser(uid string, serviceKeyPath string, fc *firebase.FirebaseComm, db storage.Storage) (string, error) { +func notifyUser(fcm *messaging.Client, uid string, fc *firebase.FirebaseComm, db storage.Storage) (string, error) { u, err := db.GetUser(uid) if err != nil { - return "", errors.Errorf("Failed to get token for UID %+v: %+v", uid, err) + jww.DEBUG.Printf("No registration found for user with ID %+v", uid) + // This path is not an error. if no results are returned, the user hasn't registered for notifications + return "", nil } - app, ctx, err := fc.SetupMessagingApp(serviceKeyPath) - if err != nil { - return "", errors.Errorf("Failed to setup messaging app: %+v", err) - } - - resp, err := fc.SendNotification(app, ctx, u.Token) + resp, err := fc.SendNotification(fcm, u.Token) if err != nil { return "", errors.Errorf("Failed to send notification to user with ID %+v: %+v", uid, err) } @@ -164,10 +177,15 @@ func notifyUser(uid string, serviceKeyPath string, fc *firebase.FirebaseComm, db // pollForNotifications accepts a gateway host and a RequestInterface (a comms object) // It retrieves a list of user ids to be notified from the gateway -func pollForNotifications(h *connect.Host, comms RequestInterface) (strings []string, e error) { - users, err := comms.RequestNotifications(h) +func pollForNotifications(nb *Impl) (strings []string, e error) { + h, ok := nb.Comms.GetHost(nb.gwId.String()) + if !ok { + return nil, errors.New("Could not find gateway host") + } + + users, err := nb.Comms.RequestNotifications(h) if err != nil { - return nil, errors.Errorf("Failed to retrieve notifications from gateway: %+v", err) + return nil, errors.Wrap(err, "Failed to retrieve notifications from gateway") } return users.IDs, nil @@ -182,7 +200,7 @@ func (nb *Impl) RegisterForNotifications(clientToken []byte, auth *connect.Auth) } err := nb.Storage.UpsertUser(u) if err != nil { - return errors.Errorf("Failed to register user with notifications: %+v", err) + return errors.Wrap(err, "Failed to register user with notifications") } return nil } @@ -191,12 +209,20 @@ func (nb *Impl) RegisterForNotifications(clientToken []byte, auth *connect.Auth) func (nb *Impl) UnregisterForNotifications(auth *connect.Auth) error { err := nb.Storage.DeleteUser(auth.Sender.GetId()) if err != nil { - return errors.Errorf("Failed to unregister user with notifications: %+v", err) + return errors.Wrap(err, "Failed to unregister user with notifications") } return nil } -// Setter function to, set NDF into our Impl structure -func (nb *Impl) updateNdf(ndf *ndf.NetworkDefinition) { +// Update stored NDF and add host for gateway to poll +func (nb *Impl) UpdateNdf(ndf *ndf.NetworkDefinition) error { + gw := ndf.Gateways[len(ndf.Gateways)-1] + nb.gwId = id.NewNodeFromBytes(ndf.Nodes[len(ndf.Nodes)-1].ID).NewGateway() + _, err := nb.Comms.AddHost(nb.gwId.String(), gw.Address, []byte(gw.TlsCertificate), true, true) + if err != nil { + return errors.Wrap(err, "Failed to add gateway host from NDF") + } + nb.ndf = ndf + return nil } diff --git a/notifications/notifications_test.go b/notifications/notifications_test.go index 7a47a048a700132b4c3ba92fed951862a4b0f7b3..dfad2cf16032a9839925014744b435f137688258 100644 --- a/notifications/notifications_test.go +++ b/notifications/notifications_test.go @@ -6,7 +6,6 @@ package notifications import ( - "context" "firebase.google.com/go/messaging" "github.com/pkg/errors" "gitlab.com/elixxir/comms/connect" @@ -14,6 +13,7 @@ import ( "gitlab.com/elixxir/notifications-bot/firebase" "gitlab.com/elixxir/notifications-bot/storage" "gitlab.com/elixxir/notifications-bot/testutil" + "gitlab.com/elixxir/primitives/id" "gitlab.com/elixxir/primitives/ndf" "gitlab.com/elixxir/primitives/utils" "os" @@ -25,56 +25,39 @@ import ( // Basic test to cover RunNotificationLoop, including error sending func TestRunNotificationLoop(t *testing.T) { impl := getNewImpl() - impl.pollFunc = func(host *connect.Host, requestInterface RequestInterface) (i []string, e error) { + impl.pollFunc = func(*Impl) (i []string, e error) { return []string{"test1", "test2"}, nil } - impl.notifyFunc = func(s3 string, s2 string, comm *firebase.FirebaseComm, storage storage.Storage) (s string, e error) { - if s3 == "test1" { - return "", errors.New("Failed to notify") - } + impl.notifyFunc = func(fcm *messaging.Client, s3 string, comm *firebase.FirebaseComm, storage storage.Storage) (s string, e error) { return "good", nil } killChan := make(chan struct{}) + errChan := make(chan error) go func() { time.Sleep(10 * time.Second) killChan <- struct{}{} }() - impl.RunNotificationLoop("", 3, killChan) + impl.RunNotificationLoop(3, killChan, errChan) } // Test notificationbot's notifyuser function // this mocks the setup and send functions, and only tests the core logic of this function func TestNotifyUser(t *testing.T) { - badsetup := func(string) (*messaging.Client, context.Context, error) { - ctx := context.Background() - return &messaging.Client{}, ctx, errors.New("Failed") - } - setup := func(string) (*messaging.Client, context.Context, error) { - ctx := context.Background() - return &messaging.Client{}, ctx, nil - } - badsend := func(firebase.FBSender, context.Context, string) (string, error) { + badsend := func(firebase.FBSender, string) (string, error) { return "", errors.New("Failed") } - send := func(firebase.FBSender, context.Context, string) (string, error) { + send := func(firebase.FBSender, string) (string, error) { return "", nil } - fc_badsetup := firebase.NewMockFirebaseComm(t, badsetup, send) - fc_badsend := firebase.NewMockFirebaseComm(t, setup, badsend) - fc := firebase.NewMockFirebaseComm(t, setup, send) + fc_badsend := firebase.NewMockFirebaseComm(t, badsend) + fc := firebase.NewMockFirebaseComm(t, send) - _, err := notifyUser("test", "testpath", fc_badsetup, testutil.MockStorage{}) - if err == nil { - t.Error("Should have returned an error") - return - } - - _, err = notifyUser("test", "testpath", fc_badsend, testutil.MockStorage{}) + _, err := notifyUser(nil, "test", fc_badsend, testutil.MockStorage{}) if err == nil { t.Errorf("Should have returned an error") } - _, err = notifyUser("test", "testpath", fc, testutil.MockStorage{}) + _, err = notifyUser(nil, "test", fc, testutil.MockStorage{}) if err != nil { t.Errorf("Failed to notify user properly") } @@ -94,31 +77,31 @@ func TestStartNotifications(t *testing.T) { PublicAddress: "0.0.0.0:42010", } - n, err := StartNotifications(params, false) + n, err := StartNotifications(params, false, true) if err == nil || !strings.Contains(err.Error(), "failed to read key at") { t.Errorf("Should have thrown an error for no key path") } params.KeyPath = wd + "/../testutil/badkey" - n, err = StartNotifications(params, false) + n, err = StartNotifications(params, false, true) if err == nil || !strings.Contains(err.Error(), "Failed to parse notifications server key") { t.Errorf("Should have thrown an error bad key") } params.KeyPath = wd + "/../testutil/cmix.rip.key" - n, err = StartNotifications(params, false) + n, err = StartNotifications(params, false, true) if err == nil || !strings.Contains(err.Error(), "failed to read certificate at") { t.Errorf("Should have thrown an error for no cert path") } params.CertPath = wd + "/../testutil/badkey" - n, err = StartNotifications(params, false) + n, err = StartNotifications(params, false, true) if err == nil || !strings.Contains(err.Error(), "Failed to parse notifications server cert") { t.Errorf("Should have thrown an error for bad certificate") } params.CertPath = wd + "/../testutil/cmix.rip.crt" - n, err = StartNotifications(params, false) + n, err = StartNotifications(params, false, true) if err != nil { t.Errorf("Failed to start notifications successfully: %+v", err) } @@ -149,21 +132,47 @@ func (m mockPollComm) RequestNotifications(host *connect.Host) (*pb.IDList, erro IDs: []string{"test"}, }, nil } +func (m mockPollComm) GetHost(hostId string) (*connect.Host, bool) { + return &connect.Host{}, true +} +func (m mockPollComm) AddHost(id, address string, cert []byte, disableTimeout, enableAuth bool) (host *connect.Host, err error) { + return nil, nil +} +func (m mockPollComm) RequestNdf(host *connect.Host, message *pb.NDFHash) (*pb.NDF, error) { + return nil, nil +} type mockPollErrComm struct{} func (m mockPollErrComm) RequestNotifications(host *connect.Host) (*pb.IDList, error) { return nil, errors.New("failed to poll") } +func (m mockPollErrComm) GetHost(hostId string) (*connect.Host, bool) { + return nil, false +} +func (m mockPollErrComm) AddHost(id, address string, cert []byte, disableTimeout, enableAuth bool) (host *connect.Host, err error) { + return nil, nil +} +func (m mockPollErrComm) RequestNdf(host *connect.Host, message *pb.NDFHash) (*pb.NDF, error) { + return nil, nil +} // Unit test for PollForNotifications func TestPollForNotifications(t *testing.T) { - _, err := pollForNotifications(nil, mockPollErrComm{}) + impl := &Impl{ + Comms: mockPollComm{}, + gwId: id.NewNodeFromBytes([]byte("test")).NewGateway(), + } + errImpl := &Impl{ + Comms: mockPollErrComm{}, + gwId: id.NewNodeFromBytes([]byte("test")).NewGateway(), + } + _, err := pollForNotifications(errImpl) if err == nil { t.Errorf("Failed to poll for notifications: %+v", err) } - _, err = pollForNotifications(nil, mockPollComm{}) + _, err = pollForNotifications(impl) if err != nil { t.Errorf("Failed to poll for notifications: %+v", err) } @@ -192,11 +201,14 @@ func TestImpl_RegisterForNotifications(t *testing.T) { func TestImpl_UpdateNdf(t *testing.T) { impl := getNewImpl() testNdf, _, err := ndf.DecodeNDF(ExampleNdfJSON) - if err != nil{ + if err != nil { t.Logf("%+v", err) } - impl.updateNdf(testNdf) + err = impl.UpdateNdf(testNdf) + if err != nil { + t.Errorf("Failed to update ndf") + } if impl.ndf != testNdf { t.Logf("Failed to change ndf") @@ -231,7 +243,8 @@ func getNewImpl() *Impl { KeyPath: wd + "/../testutil/cmix.rip.key", CertPath: wd + "/../testutil/cmix.rip.crt", PublicAddress: "0.0.0.0:0", + FBCreds: "", } - instance, _ := StartNotifications(params, false) + instance, _ := StartNotifications(params, false, true) return instance } diff --git a/notifications/updateNDF.go b/notifications/updateNDF.go index ed8dafb9e26c995380b44c877a2dd05b5738c693..d8588987d56ddea14b355a288ac323bd768ca30a 100644 --- a/notifications/updateNDF.go +++ b/notifications/updateNDF.go @@ -12,7 +12,6 @@ import ( "crypto/sha256" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" - "gitlab.com/elixxir/comms/connect" pb "gitlab.com/elixxir/comms/mixmessages" "gitlab.com/elixxir/primitives/id" "gitlab.com/elixxir/primitives/ndf" @@ -21,22 +20,19 @@ import ( var noNDFErr = errors.Errorf("Permissioning server does not have an ndf to give to client") -// We use an interface here inorder to allow us to mock the getHost and RequestNDF in the notifcationsBot.Comms for testing -type notificationComms interface { - GetHost(hostId string) (*connect.Host, bool) - RequestNdf(host *connect.Host, message *pb.NDFHash) (*pb.NDF, error) -} - // PollNdf, attempts to connect to the permissioning server to retrieve the latest ndf for the notifications bot -func PollNdf(currentDef *ndf.NetworkDefinition, comms notificationComms) (*ndf.NetworkDefinition, error) { +func PollNdf(currentDef *ndf.NetworkDefinition, comms NotificationComms) (*ndf.NetworkDefinition, error) { //Hash the notifications bot ndf for comparison with registration's ndf - hash := sha256.New() - ndfBytes := currentDef.Serialize() - hash.Write(ndfBytes) - ndfHash := hash.Sum(nil) + var ndfHash []byte + if currentDef != nil { + hash := sha256.New() + ndfBytes := currentDef.Serialize() + hash.Write(ndfBytes) + ndfHash = hash.Sum(nil) + } //Put the hash in a message - msg := &pb.NDFHash{Hash: ndfHash} + msg := &pb.NDFHash{Hash: ndfHash} // TODO: this should be a helper somewhere regHost, ok := comms.GetHost(id.PERMISSIONING) if !ok { @@ -46,8 +42,8 @@ func PollNdf(currentDef *ndf.NetworkDefinition, comms notificationComms) (*ndf.N //Send the hash to registration response, err := comms.RequestNdf(regHost, msg) if err != nil { - errMsg := errors.Errorf("Failed to get ndf from permissioning: %v", err) - if strings.Contains(errMsg.Error(), noNDFErr.Error()) { + errMsg := errors.Wrap(err, "Failed to get ndf from permissioning") + if strings.Contains(errMsg.Error(), noNDFErr.Error()) { jww.WARN.Println("Continuing without an updated NDF") return nil, nil } diff --git a/notifications/updateNDF_test.go b/notifications/updateNDF_test.go index 75a0220b4ebe1a215f1839d16219777d82cb34a5..26048c70f5a75490cbc14046b3d715a704c53f92 100644 --- a/notifications/updateNDF_test.go +++ b/notifications/updateNDF_test.go @@ -39,7 +39,7 @@ func TestPollNdf(t *testing.T) { // Test that pollNdf returns an error in this case RequestNdfErr = errors.New("Permissioning server does not have an ndf to give to client") GetHostErrBool = true - testNdf ,err := PollNdf(newNdf, mockNotificationComms{}) + testNdf, err := PollNdf(newNdf, mockNotificationComms{}) if err != nil && testNdf != nil { t.Logf("RequestNdf should have returned nil for everything because there is no new ndf but didnt") @@ -78,3 +78,11 @@ func (m mockNotificationComms) GetHost(hostId string) (*connect.Host, bool) { func (m mockNotificationComms) RequestNdf(host *connect.Host, message *pb.NDFHash) (*pb.NDF, error) { return &NdfToreturn, RequestNdfErr } + +func (m mockNotificationComms) RequestNotifications(host *connect.Host) (*pb.IDList, error) { + return nil, errors.New("failed to poll") +} + +func (m mockNotificationComms) AddHost(id, address string, cert []byte, disableTimeout, enableAuth bool) (host *connect.Host, err error) { + return nil, nil +}