Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multi-tenant config #564

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

marius-bardan
Copy link

@marius-bardan marius-bardan commented Mar 25, 2021

This should address #99 and #365

@McPo
Copy link
Contributor

McPo commented Mar 25, 2021

We are also currently in need of this feature. As such we ended up creating our own patch a couple years ago, however at the time there appeared to be some reluctance to incorporate this feature.

So our approach might be a bit more limited than the above but it also has less config and a few other advantages. Our use case is that we have multiple white-labelled apps, in the case of FCM this is easily solved as you can use the same token for multiple apps. However for iOS you need to generate a separate cert for each app. Our solution was to place all the certs in the same pem file and then read the Subject Name to pull out the correct key. However this means the JWT approach for iOS is not supported. (Im not sure if it could be supported, I simply haven't tried)

We then pass the bundle-id as a prefix to the topic in the gorush request (Maybe in the case of the JWT token, we could just pass it in, instead of an id. And maybe we create a new key called "tenant" instead). This has a slight advantage over the URL path taking in this PR, because it mean the 3rd-Party app can just make one request to GoRush, and its GoRush's job to split the request. i.e.({"notifications":[{"topic":"app-a-id.topic"},{"topic":"app-b-id.topic"}]}) In the case that no cert with that ID exists, we fallback to the cert defined at the top of the pem.

Anyway, theres not much point to this post beyond just documenting one of the ways we solved this issue and some of the advantages it has. (It suited us well, as we mounted the cert file in docker, so if we needed to add another white-label, we didn't need to change the gorush config, we simply added the cert to the pem file.

I wonder if this could be duplicated in this PR, essentially maybe mount a directory thats list all the cert file named by the app bundle id i.e. "my.app.pem"? (Should we have an equivalent for FCM/JWT?). And maybe move this PR away from using a different URL endpoint, and instead support adding a bundleID into the list of notifications, so the third party server cans still just make one request and have gorush deal with fanning it out.

diff --git a/gorush/global.go b/gorush/global.go
index b97130d..6a7f3db 100644
--- a/gorush/global.go
+++ b/gorush/global.go
@@ -15,7 +15,7 @@ var (
 	// QueueNotification is chan type
 	QueueNotification chan PushNotification
 	// ApnsClient is apns client
-	ApnsClient *apns2.Client
+	ApnsClientMap map[string]*apns2.Client
 	// FCMClient is apns client
 	FCMClient *fcm.Client
 	// LogAccess is log server request log
diff --git a/gorush/notification_apns.go b/gorush/notification_apns.go
index c4fe903..3205d36 100644
--- a/gorush/notification_apns.go
+++ b/gorush/notification_apns.go
@@ -9,6 +9,8 @@ import (
 	"path/filepath"
 	"sync"
 	"time"
+	"regexp"
+	"io/ioutil"
 
 	"github.com/mitchellh/mapstructure"
 	"github.com/sideshow/apns2"
@@ -33,7 +35,7 @@ func InitAPNSClient() error {
 	if PushConf.Ios.Enabled {
 		var err error
 		var authKey *ecdsa.PrivateKey
-		var certificateKey tls.Certificate
+		var certificateKeys map[string]tls.Certificate
 		var ext string
 
 		if PushConf.Ios.KeyPath != "" {
@@ -41,9 +43,9 @@ func InitAPNSClient() error {
 
 			switch ext {
 			case ".p12":
-				certificateKey, err = certificate.FromP12File(PushConf.Ios.KeyPath, PushConf.Ios.Password)
+				certificateKeys[""], err = certificate.FromP12File(PushConf.Ios.KeyPath, PushConf.Ios.Password)
 			case ".pem":
-				certificateKey, err = certificate.FromPemFile(PushConf.Ios.KeyPath, PushConf.Ios.Password)
+				certificateKeys, err = FromBundledPemFile(PushConf.Ios.KeyPath, PushConf.Ios.Password)
 			case ".p8":
 				authKey, err = token.AuthKeyFromFile(PushConf.Ios.KeyPath)
 			default:
@@ -65,9 +67,9 @@ func InitAPNSClient() error {
 			}
 			switch ext {
 			case ".p12":
-				certificateKey, err = certificate.FromP12Bytes(key, PushConf.Ios.Password)
+				certificateKeys[""], err = certificate.FromP12Bytes(key, PushConf.Ios.Password)
 			case ".pem":
-				certificateKey, err = certificate.FromPemBytes(key, PushConf.Ios.Password)
+				certificateKeys[""], err = certificate.FromPemBytes(key, PushConf.Ios.Password)
 			case ".p8":
 				authKey, err = token.AuthKeyFromBytes(key)
 			default:
@@ -81,6 +83,7 @@ func InitAPNSClient() error {
 			}
 		}
 
+		ApnsClientMap = make(map[string]*apns2.Client)
 		if ext == ".p8" {
 			if PushConf.Ios.KeyID == "" || PushConf.Ios.TeamID == "" {
 				msg := "You should provide ios.KeyID and ios.TeamID for P8 token"
@@ -90,9 +93,11 @@ func InitAPNSClient() error {
 				TeamID: PushConf.Ios.TeamID,
 			}
 
-			ApnsClient, err = newApnsTokenClient(token)
+			ApnsClientMap[""], err = newApnsTokenClient(token)
 		} else {
-			ApnsClient, err = newApnsClient(certificateKey)
+			for k, v := range certificateKeys {
+				ApnsClientMap[k], err = newApnsClient(v)
+			}
 		}
 
 		if err != nil {
@@ -105,6 +110,30 @@ func InitAPNSClient() error {
 	return nil
 }
 
+func FromBundledPemFile(filename string, password string) (map[string]tls.Certificate, error) {
+	var certBundleRegex = regexp.MustCompile(`(?ms)(^-*BEGIN CERTIFICATE-*$)(.*?)(^-*END RSA PRIVATE KEY-*$)`)
+
+	var certificateMap map[string]tls.Certificate
+	certificateMap = make(map[string]tls.Certificate)
+	bytes, err := ioutil.ReadFile(filename)
+	if err != nil {
+		return certificateMap, err
+	}
+
+	var certs = certBundleRegex.FindAllStringSubmatch(string(bytes), -1)
+	var cert tls.Certificate
+	var k string
+	// Reverse iterate. So last cert processed is on top
+	for i := range certs { i = len(certs) - 1 - i
+		cert, err = certificate.FromPemBytes([]byte(certs[i][0]), password)
+		k = cert.Leaf.Subject.Names[0].Value.(string)
+		certificateMap[k] = cert
+	}
+	certificateMap[""] = cert
+
+	return certificateMap, err
+}
+
 func newApnsClient(certificate tls.Certificate) (*apns2.Client, error) {
 	var client *apns2.Client
 
@@ -320,7 +349,12 @@ func GetIOSNotification(req PushNotification) *apns2.Notification {
 	return notification
 }
 
+var topicToBundleId= regexp.MustCompile(`^(.*)(\.voip|\.complication|\.pushkit\.fileprovider)$`)
 func getApnsClient(req PushNotification) (client *apns2.Client) {
+	var bundleID = topicToBundleId.ReplaceAllString(req.Topic, `$1`)
+	var ApnsClient, exists = ApnsClientMap[bundleID]
+	if !exists { ApnsClient = ApnsClientMap[""] }
+
 	if req.Production {
 		client = ApnsClient.Production()
 	} else if req.Development {

@appleboy appleboy force-pushed the master branch 2 times, most recently from 60a7a68 to 2c29b4b Compare June 30, 2022 15:02
@appleboy appleboy force-pushed the master branch 4 times, most recently from bff0f2d to 6b6ef69 Compare December 24, 2022 13:02
# Conflicts:
#	README.md
#	config/config.go
#	config/testdata/config.yml
#	gorush/const.go
#	gorush/global.go
#	gorush/log_test.go
#	gorush/notification_apns_test.go
#	gorush/notification_hms.go
#	gorush/notification_hms_test.go
#	gorush/notification_test.go
#	gorush/server.go
#	gorush/server_test.go
#	gorush/worker.go
#	logx/log.go
#	main.go
#	notify/feedback_test.go
#	notify/notification.go
#	notify/notification_apns.go
#	notify/notification_fcm.go
#	notify/notification_fcm_test.go
#	rpc/server.go
#	rpc/server_test.go
@obuzyig
Copy link

obuzyig commented Jun 10, 2024

With the improvement of the FCM HTTP v1 API, there might be some opportunity to bring multi-tenant competencies on the table. I think we need to store multiple application's bearer tokens produced during the authorization.

Can't we use this environmental variable capacities to adapt the application?

@appleboy
Copy link
Owner

@obuzyig I will take it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants