Using your own cloud function for ES

oZoneDev
zmNinja
Published in
5 min readJul 17, 2021

Updated Jul 21, 2022: Legacy style FCM won’t work for iOS. May work for Android.

Updated Jun 28,2022: I switched off my server on Jun 24, which means you won’t be getting pushes. While the ZM team launches a new client and their cloud function, you can fall back to the old FCM style in ES that will not require my server. You may lose some functionality (forgot what) but you’ll then start getting pushes again. To do that, just change this in zmeventnotification.ini and restart the ES:

use_fcmv1 = no

Thanks for that reminder dave88!

Updated, Sep 25: 2021: In addition to setting up your own cloud function, you will likely have to recompile zmNinja to use the serviceID that is associated to your own google account. As it stands right now, zmNinja is compiled using my google service account and push tokens are generated so that only projects associated to a specific service ID are able to send push messages to zmNinja. Follow this post — its currently investigation in progress as a user is trying to get things set up.

This post is in continuation of zmNinja+ES+ML EOL and covers what you need to do to run your own cloud function. Note that as of Jul 17, ZM Devs have shown interest in migrating the cloud function to their own set up in which case you may not have to do anything (except for upgrading ZMES which will have the new cloud function URL). I plan to continue my cloud function till Year End and if ZM devs take over, I’ll make the switch in the next version of the ES.

Basics

  • FCMv1 changed the way authentication needs to be done. Instead of a static key like before, Google Firebase servers exchange OAuth keys with a trusted app. The trusted app and the google servers share a secret that is negotiated between them.
  • So I set up a “google cloud function” (equivalent to AWS serverless/lambdas) in python that basically uses their firebase python messaging SDK (docs). This is the “Trusted App” that is authorized to send messages via Google’s firebase servers
  • When you send a message from your ES, it reaches this “cloud function” and the cloud function forwards to google FCM servers

How to set up a python cloud function in GCP

  • There are many tutorials. See this.
  • Before you go into replicating my code, just make sure you are able to set up a function that has a public URL that returns a “hello world” or something on the browser and then you can start messing with the rest

Authorization between your ES and the Trusted App

  • I chose to set up a JWT auth mechanism between your ES and my Trusted App. If your JWT token doesn’t match what I expect, then I’ll kick it out in my trusted app.
  • The token that the ES will use depends on the token you specify in fcm_v1_key. If you leave it empty, the default value of my key is used here.
  • The ES reaches the ‘Trusted App’ via the URL specified in fcm_v1_url. If you leave it empty, the default URL of my cloud function is used here.

Authorization between the Trusted App and Google Firebase servers

  • This is automatically handled by the firebase admin SDK in python — given I hosted it as a GCP cloud function, all I need to do is initialize the app and Google takes care of authentication. If we host it elsewhere, there are instructions on how to authenticate. I just took the easy route.
  • Note that Google allows for authenticated requests to reach cloud function. In my case, I set this to “allow unauthenticated” so anyone could send a push request to my cloud function as long as their JWT token is valid. However, to actually reach a device, they’d need the device token ID, which only your ES will have. This should be improved in the future to only allow authenticated requests and then have the ES and the Trusted App exchange auth info even for sending requests

What needs to happen when the “Trusted App” moves from me to whoever takes it over (including hosting it on your own for your own needs)

  • You need to replicate my code as a cloud function somewhere (I’d recommend GCP as its easy)
  • You will need to change DEFAULT_FCM_V1_URL in zmeventnotification.pl to your cloud function URL or simply use a custom value in fcm_v1_url in zmeventnotification.ini. For example, today it is ‘https://us-central1-ninja-1105.cloudfunctions.net/send_push' but when you set up your own, you’ll get a different URL
  • Decide on a ‘secret key’. This ‘secret key’ will be stored in your cloud function and then you will use that secret key to sign a JWT token on the ES side. Let’s go through an example:

Let’s suppose you decide on a secret of ‘i-love-zmninja’. Then change this line in main.py to:

SECRET='i-love-zmninja'

You now need to create a token to use on the ES side that is signed by this secret. An easy way to generate the token is to simply use python. You will need pyjwt installed ( pip3 install pyjwt ). Launch up the python interpreter and type these commands in: (don’t type in >>> )

>>> import datetime
>>> import jwt
>>> jwt.encode({'generator': 'pliable pixels', 'iat':datetime.datetime.utcnow(),
'client':'zmninja'}, 'i-love-zmninja', algorithm='HS256')

This will produce an output like this:

b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJnZW5lcmF0b3IiOiJwbGlhYmxlIHBpeGVscyIsImlhdCI6MTYzMDg3MDMyNiwiY2xpZW50Ijoiem1uaW5qYSJ9.IVTP7dAA_86PrSirIexXqlt2cV2E5yh8DOXutkyDZpY'

So here:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJnZW5lcmF0b3IiOiJwbGlhYmxlIHBpeGVscyIsImlhdCI6MTYzMDg3MDMyNiwiY2xpZW50Ijoiem1uaW5qYSJ9.IVTP7dAA_86PrSirIexXqlt2cV2E5yh8DOXutkyDZpY

is your token. (ignore the b and single quotes at start and end) This is the value you put in fcm_v1_key in zmeventnotification.ini, or, you can change DEFAULT_FCM_V1_KEY in zmeventnotification.pl

Now the ES will send the token to the cloud function. The cloud function will decode the token using the secret and since the token was signed with this secret, everything should work.

The cloud function code

When you set up the python cloud function, two files are created:

  • main.py — which holds the actual function code (entry point is the name you specify when creating the cloud function, in my case, it is called send_push). Source code here
  • requirements.txt — dependencies that get installed when the cloud function is invoked for the first time (remember these are ephemeral — these functions spin up, then are removed when there isn’t constant traffic for a while and then when the next request comes in, it spins up again — all of this is automatic). Source code here

--

--

oZoneDev
zmNinja

A breath of fresh air for security and surveillance software