Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F7159306
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
12 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/pushserver/pns/firebase.py b/pushserver/pns/firebase.py
index 2c7ca7e..f092ea6 100644
--- a/pushserver/pns/firebase.py
+++ b/pushserver/pns/firebase.py
@@ -1,351 +1,351 @@
import json
import os
import time
from datetime import datetime
import oauth2client
import requests
from oauth2client.service_account import ServiceAccountCredentials
from pushserver.models.requests import WakeUpRequest
from requests.adapters import HTTPAdapter
from urllib3 import Retry
from pushserver.pns.base import PNS, PushRequest, PlatformRegister
from pushserver.resources.utils import log_event, fix_non_serializable_types
class FirebasePNS(PNS):
"""
A Firebase Push Notification service
"""
def __init__(self, app_id: str, app_name: str, url_push: str,
voip: bool, auth_key: str = None, auth_file: str = None):
"""
:param app_id `str`: Application ID.
:param url_push `str`: URI to push a notification.
:param voip `bool`: required for apple, `True` for voip push notification type.
:param auth_key `str`: A Firebase credential for push notifications.
:param auth_file `str`: A Firebase credential for push notifications.
"""
self.app_id = app_id
self.app_name = app_name
self.url_push = url_push
self.voip = voip
self.auth_key = auth_key
self.auth_file = auth_file
self.error = ''
self.last_update_token = None
self.access_token = self.set_access_token()
def set_access_token(self) -> str:
"""
Retrieve a valid access token that can be used to authorize requests.
:return: `str` Access token.
"""
if not self.auth_file:
self.error = f"Cannot generated Firebase access token, " \
f"no auth file provided"
return ''
if not os.path.exists(self.auth_file):
self.error = f"Cannot generated Firebase access token, " \
f"auth file {self.auth_file} not found"
return ''
scopes = ['https://www.googleapis.com/auth/firebase.messaging']
try:
credentials = ServiceAccountCredentials.from_json_keyfile_name(self.auth_file,
scopes)
oauth2client.client.logger.setLevel('CRITICAL')
access_token_info = credentials.get_access_token()
self.last_update_token = datetime.now()
return access_token_info.access_token
except Exception as e:
self.error = f"Error: cannot generated Firebase access token: {e}"
return ''
class FirebaseRegister(PlatformRegister):
def __init__(self, app_id: str, app_name: str, voip: bool,
config_dict: dict, credentials_path: str, loggers: dict):
self.app_id = app_id
self.app_name = app_name
self.voip = voip
self.credentials_path = credentials_path
self.config_dict = config_dict
self.loggers = loggers
self.error = ''
self.auth_key, self.auth_file = self.set_auths()
@property
def url_push(self):
try:
return self.config_dict['firebase_push_url']
except KeyError:
self.error = 'firebase_push_url not found in applications.ini'
return None
def set_auths(self):
auth_key = None
auth_file = None
try:
auth_key = self.config_dict['firebase_authorization_key']
except KeyError:
try:
auth_file = self.config_dict['firebase_authorization_file']
if self.credentials_path:
auth_file = f"{self.credentials_path}/" \
f"{auth_file}"
else:
pass
if not os.path.exists(auth_file):
self.error = f'{auth_file} - no such file'
except KeyError:
self.error = 'not firebase_authorization_key or ' \
'firebase_authorization_file found in applications.ini'
return auth_key, auth_file
@property
def pns(self) -> FirebasePNS:
pns = None
if self.auth_key:
auth_file = ''
pns = FirebasePNS(app_id=self.app_id,
app_name=self.app_name,
url_push=self.url_push,
voip=self.voip,
auth_key=self.auth_key,
auth_file=auth_file)
elif self.auth_file:
pns = FirebasePNS(app_id=self.app_id,
app_name=self.app_name,
url_push=self.url_push,
voip=self.voip,
auth_file=self.auth_file)
self.error = pns.error if pns.error else ''
return pns
@property
def register_entries(self):
if self.error:
return {}
return {'pns': self.pns,
'access_token': self.pns.access_token,
'auth_key': self.auth_key,
'auth_file': self.auth_file}
class FirebasePushRequest(PushRequest):
"""
Firebase push notification request
"""
def __init__(self, error: str, app_name: str, app_id: str,
request_id: str, headers: str, payload: dict,
loggers: dict, log_remote: dict,
wp_request: WakeUpRequest, register: dict):
"""
:param error: `str`
:param app_name: `str` 'linphone' or 'payload'
:param app_id: `str` bundle id
:param headers: `AppleHeaders` Apple push notification headers
:param payload: `ApplePayload` Apple push notification payload
:param wp_request: `WakeUpRequest`
:param loggers: `dict` global logging instances to write messages (params.loggers)
"""
self.error = error
self.app_name = app_name
self.app_id = app_id
self.platform = 'firebase'
self.request_id = request_id
self.headers = headers
self.payload = payload
self.token = wp_request.token
self.wp_request = wp_request
self.loggers = loggers
self.log_remote = log_remote
self.pns = register['pns']
self.path = self.pns.url_push
self.results = self.send_notification()
def requests_retry_session(self, counter=0):
"""
Define parameters to retry a push notification
according to media_type.
:param counter: `int` (optional) if retries was necessary
because of connection fails
Following rfc3261 specification, an exponential backoff factor is used.
More specifically:
backoff = 0.5
T1 = 500ms
max_retries_call = 7
time_to_live_call = 64 seconds
max_retries_sms = 11
time_to_live_sms ~ 2 hours
"""
retries = self.retries_params[self.media_type] - counter
backoff_factor = self.retries_params['bo_factor'] * 0.5 * 2 ** counter
status_forcelist = tuple([status for status in range(500, 600)])
session = None
session = session or requests.Session()
retry = Retry(
total=retries,
read=retries,
connect=retries,
backoff_factor=backoff_factor,
status_forcelist=status_forcelist)
adapter = HTTPAdapter(max_retries=retry)
session.mount('http://', adapter)
session.mount('https://', adapter)
return session
def send_notification(self) -> dict:
"""
Send a Firebase push notification
"""
if self.error:
self.log_error()
return {'code': 500, 'body': {}, 'reason': 'Internal server error'}
n_retries, backoff_factor = self.retries_params(self.wp_request.media_type)
counter = 0
error = False
code = 500
reason = ""
body = None
response = None
while counter <= n_retries:
self.log_request(path=self.pns.url_push)
try:
response = requests.post(self.pns.url_push,
self.payload,
headers=self.headers)
break
except requests.exceptions.RequestException as e:
error = True
reason = f'connection failed: {e}'
counter += 1
timer = backoff_factor * (2 ** (counter - 1))
time.sleep(timer)
if counter == n_retries:
reason = "maximum retries reached"
elif error:
try:
response = self.requests_retry_session(counter). \
post(self.pns.url_push,
self.payload,
headers=self.headers)
except Exception as x:
level = 'error'
msg = f"outgoing {self.platform.title()} response for " \
f"{self.request_id}, push failed: " \
f"an error occurred in {x.__class__.__name__}"
log_event(loggers=self.loggers, msg=msg, level=level)
try:
body = response.__dict__
except (TypeError, ValueError):
code = 500
reason = 'cannot parse response body'
body = {}
else:
reason = body.get('reason')
code = response.status_code
for k in ('raw', 'request', 'connection', 'cookies', 'elapsed'):
try:
del body[k]
except KeyError:
pass
except TypeError:
break
body = json.dumps(fix_non_serializable_types(body))
if isinstance(body, str):
body = json.loads(body)
if code == 200:
try:
- failure = body['data']['body']['_content']['failure']
+ failure = body['_content']['failure']
except KeyError:
pass
else:
if failure == 1:
- reason = body['data']['body']['_content']['results'][0]['error']
+ reason = body['_content']['results'][0]['error']
code = 410
elif code == 404:
try:
- payload_code = body['data']['body']['_content']['error']['code']
+ payload_code = body['_content']['error']['code']
except KeyError:
pass
else:
if payload_code == 404:
code = 410
keys = list(body.keys())
for key in keys:
if not body[key]:
del body[key]
results = {'body': body,
'code': code,
'reason': reason,
'url': self.pns.url_push,
'call_id': self.wp_request.call_id,
'token': self.token
}
self.results = results
self.log_results()
# Request is missing required authentication credential.
# Expected OAuth 2 access token, login cookie or other valid authentication
# credential. UNAUTHENTICATED
code = results.get('code')
reason = results.get('reason')
if code == 401 and reason == 'Unauthorized':
delta = datetime.now() - self.pns.last_update_token
if delta.total_seconds() > 300: # 5 min
level = 'warn'
msg = f"outgoing {self.platform.title()} response for request " \
f"{self.request_id} need a new access token - " \
f"server will refresh it and try again"
log_event(loggers=self.loggers, msg=msg, level=level, to_file=True)
# retry with a new Fireplace access token
self.pns.access_token = self.pns.set_access_token()
level = 'warn'
msg = f"outgoing {self.platform.title()} response for request " \
f"{self.request_id} a new access token was generated - " \
f"trying again"
log_event(loggers=self.loggers, msg=msg, level=level, to_file=True)
self.results = self.send_notification()
return results
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Nov 23, 4:04 AM (20 h, 13 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3408775
Default Alt Text
(12 KB)
Attached To
Mode
rSYLKPUSH Sylk Pushserver
Attached
Detach File
Event Timeline
Log In to Comment