diff --git a/pushserver/applications/sylk.py b/pushserver/applications/sylk.py index f92e71a..d0f1ba1 100644 --- a/pushserver/applications/sylk.py +++ b/pushserver/applications/sylk.py @@ -1,208 +1,208 @@ -import datetime +# import datetime from pushserver.applications.apple import * from pushserver.applications.firebase import * from pushserver.resources.utils import callid_to_uuid -#from firebase_admin import messaging +# from firebase_admin import messaging __all__ = ['AppleSylkHeaders', 'AppleSylkPayload', 'FirebaseSylkHeaders', 'FirebaseSylkPayload'] class AppleSylkHeaders(AppleHeaders): """ An Apple headers structure for a push notification """ def create_push_type(self) -> str: """ logic to define apns_push_type value using request parameters apns_push_type reflect the contents of the notification’s payload, it can be: 'alert', 'background', 'voip', 'complication', 'fileprovider' or 'mdm'. """ push_type = 'alert' if self.event in ('incoming_session', 'incoming_conference_request'): push_type = 'voip' elif self.event == 'cancel': push_type = 'background' return push_type def create_expiration(self) -> int: """ logic to define apns_expiration value using request parameters apns_expiration is the date at which the notification expires, (UNIX epoch expressed in seconds UTC). """ return '120' def create_topic(self) -> str: """ logic to define apns_topic value using request parameters apns_topic is in general is the app’s bundle ID and may have a suffix based on the notification’s type. """ apns_topic = self.app_id if self.app_id.endswith('.dev') or self.app_id.endswith('.prod'): apns_topic = '.'.join(self.app_id.split('.')[:-1]) if self.event in ('incoming_session', 'incoming_conference_request'): apns_topic = f"{apns_topic}.voip" return apns_topic def create_priority(self) -> int: """ logic to define apns_priority value using request parameters Notification priority, apns_prioriy 10 o send the notification immediately, 5 to send the notification based on power considerations on the user’s device. """ apns_priority = '10' if self.event in ('incoming_session', 'incoming_conference_request') else '5' return apns_priority class FirebaseSylkHeaders(FirebaseHeaders): """ Firebase headers for a push notification """ class AppleSylkPayload(ApplePayload): """ A payload for a Apple Sylk push notification """ @property def payload(self) -> str: """ Generate an AppleSylk notification payload """ if self.event == 'cancel': payload = { 'event': self.event, 'call-id': self.call_id, 'session-id': callid_to_uuid(self.call_id), 'reason': self.reason } elif self.event == 'message': payload = { 'aps': { 'alert': { 'title' : 'New message', 'body' : 'From %s' % self.sip_from, }, 'message_id': self.call_id, "sound" : "default", "badge" : self.badge, } } else: payload = { 'event': self.event, 'call-id': self.call_id, 'session-id': callid_to_uuid(self.call_id), 'media-type': self.media_type, 'from_uri': self.sip_from, 'from_display_name': self.from_display_name, 'to_uri': self.sip_to } return payload class FirebaseSylkPayload(FirebasePayload): """ A payload for a Firebase Sylk push notification """ @property def payload(self) -> str: """ Generate a Sylk payload and extra Firebase parameters """ if not self.from_display_name: from_display_name = self.sip_from else: from_display_name = self.from_display_name if self.event == 'cancel': data = { 'event': self.event, 'call-id': self.call_id, 'session-id': callid_to_uuid(self.call_id), 'reason': self.reason } elif self.event == 'message': data = { 'event': self.event, 'from_uri': self.sip_from, 'to_uri': self.sip_to } else: data = { 'event': self.event, 'call-id': self.call_id, 'session-id': callid_to_uuid(self.call_id), 'media-type': self.media_type, 'from_uri': self.sip_from, 'from_display_name': from_display_name, 'to_uri': self.sip_to } http_payload = { 'message': { 'token': self.token, 'data': data, 'android': { 'priority': 'high', 'ttl': '60s' } } } if (self.event == 'message'): http_payload |= { 'message': { 'token': self.token, 'data': data, 'notification': { 'title': 'New message', 'body': 'From %s' % self.sip_from, 'image': 'https://icanblink.com/apple-touch-icon-180x180.png' }, 'apns': { 'headers': { 'apns-priority': '5', } }, 'android': { 'priority': 'high', 'ttl': '60s', 'notification': { 'channel_id': 'sylk-messages-sound', 'sound': 'default', 'default_sound': True, 'notification_priority': 'PRIORITY_HIGH' } } } } # fcm_payload = messaging.Message( # token=self.token, # data=data, # android=messaging.AndroidConfig( # ttl=datetime.timedelta(seconds=60), # priority='high' # ) # ) return http_payload diff --git a/pushserver/pns/apple.py b/pushserver/pns/apple.py index c2335a4..8d80022 100644 --- a/pushserver/pns/apple.py +++ b/pushserver/pns/apple.py @@ -1,402 +1,402 @@ import json import os import socket import ssl import time import hyper from hyper import HTTP20Connection, tls from pushserver.models.requests import WakeUpRequest from pushserver.resources.utils import log_event, ssl_cert from pushserver.pns.base import PNS, PushRequest, PlatformRegister class ApplePNS(PNS): """ An Apple Push Notification service """ def __init__(self, app_id: str, app_name: str, url_push: str, voip: bool, cert_file: str, key_file: str): """ :param app_id: `str`, blunde id provided by application. :param url_push: `str`, URI to push a notification (from applications.ini) :param cert_file `str`: path to APNS certificate (provided by dev app kit) :param key_file `str`: path to APNS key (provided by dev app kit) :param voip: `bool`, Required for apple, `True` for voip push notification type. """ self.app_id = app_id self.app_name = app_name self.url_push = url_push self.voip = voip self.key_file = key_file self.cert_file = cert_file class AppleConn(ApplePNS): """ An Apple connection """ def __init__(self, app_id: str, app_name: str, url_push: str, voip: bool, cert_file: str, key_file: str, apple_pns: PNS, loggers: dict, port: int = 443): """ :param apple_pns `ApplePNS`: Apple Push Notification Service. :param port `int`: 443 or 2197 to allow APNS traffic but block other HTTP traffic. :param loggers: `dict` global logging instances to write messages (params.loggers) :attribute ssl_context `ssl.SSLContext`: generated with a valid apple certificate. :attribute connection `HTTP20Connection`: related to an app and its corresponding certificate. """ self.app_id = app_id self.app_name = app_name self.url_push = url_push self.voip = voip self.key_file = key_file self.cert_file = cert_file self.apple_pns = apple_pns self.port = port self.loggers = loggers @property def ssl_context(self) -> ssl.SSLContext: """ Define a ssl context using a cert_file to open a connection requires a valid certificate file :return: a ssl.SSLContext object """ cert_file = self.cert_file key_file = self.key_file if self.key_file else self.cert_file ssl_context = ssl.create_default_context() ssl_context.check_hostname = False ssl_context.verify_mode = ssl.CERT_NONE ssl_context.load_cert_chain(keyfile=key_file, certfile=cert_file) return ssl_context @property def connection(self) -> HTTP20Connection: """ Open an apple connection requires a ssl context :return: an hyper.http20.connection.HTTP20Connection object """ host = self.url_push port = self.port ssl_context = self.ssl_context connection = HTTP20Connection(host=host, port=port, ssl_context=ssl_context, force_proto=tls.H2C_PROTOCOL) cert_file_name = self.cert_file.split('/')[-1] key_file_name = self.key_file.split('/')[-1] if self.key_file else None if key_file_name: msg = f'{self.app_name.capitalize()} app: Connecting to {host}:{port} ' \ f'using {cert_file_name} certificate ' \ f'and {key_file_name} key files' else: msg = f'{self.app_name.capitalize()} app: Connecting to {host}:{port} ' \ f'using {cert_file_name} certificate' log_event(loggers=self.loggers, msg=msg, level='deb') return connection class AppleRegister(PlatformRegister): def __init__(self, app_id: str, app_name: str, voip: bool, credentials_path: str, config_dict: dict, 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 = '' @property def url_push(self) -> str: try: return self.config_dict['apple_push_url'] except KeyError: self.error = 'apple_push_url not found in applications.ini' return None @property def certificate(self) -> dict: if self.error: return {} else: try: cert_file = f"{self.credentials_path}/" \ f"{self.config_dict['apple_certificate']}" cert_exists = os.path.exists(cert_file) if not cert_exists: self.error = f"{cert_file} - no such file." return {} else: return {'cert_file': cert_file, 'cert_exists': cert_exists} except KeyError: self.error = 'apple_certificate not found in applications.ini' return {} @property def key(self) -> dict: if self.error: return {} try: key_file = f"{self.credentials_path}/" \ f"{self.config_dict['apple_key']}" key_exists = os.path.exists(key_file) if not key_exists: self.error = f"{key_file} - no such file." return {} except KeyError: return {} return {'key_file': key_file, 'key_exists': key_exists} @property def ssl_valid_cert(self) -> bool: if self.error: return else: try: cert_file = self.certificate.get('cert_file') key_file = self.key.get('key_file') if not (cert_file or key_file): self.error = 'An apple certificate/key is needed to open a connection' elif not ssl_cert(cert_file, key_file): self.error = f"{cert_file} - bad ssl certificate." return else: return True except FileNotFoundError as exc: self.error = exc return @property def apple_pns(self) -> ApplePNS: if self.error: return if self.ssl_valid_cert: cert_file = self.certificate.get('cert_file') key_file = self.key.get('key_file') return ApplePNS(app_id=self.app_id, app_name=self.app_name, url_push=self.url_push, voip=self.voip, cert_file=cert_file, key_file=key_file) @property def apple_conn(self): if self.error: return return AppleConn(app_id=self.app_id, app_name=self.app_name, url_push=self.url_push, voip=self.voip, cert_file=self.certificate.get('cert_file'), key_file=self.key.get('key_file'), apple_pns=self.apple_pns, loggers=self.loggers).connection @property def register_entries(self): if self.error: return {} return {'pns': self.apple_pns, 'conn': self.apple_conn} class ApplePushRequest(PushRequest): """ Apple 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 = 'apple' self.request_id = request_id self.headers = headers self.payload = payload self.token = wp_request.token self.call_id = wp_request.call_id self.media_type = wp_request.media_type self.wp_request = wp_request self.loggers = loggers self.log_remote = log_remote self.apple_pns = register['pns'] self.connection = register['conn'] self.path = f'/3/device/{self.token}' self.results = self.send_notification() def send_notification(self) -> dict: """ Send an apple push requests to a single device. If status of response is like 5xx, an exponential backoff factor is implemented to retry the notification according to media type. :param `hstr` token: destination device. :param `str` method: HTTP request method, must be 'POST'. :param `AppleHeaders` headers: Apple push notification headers. :param `ApplePayload` payload: Apple push notification payload. """ if self.error: self.log_error() return {'code': 500, 'body': {}, 'reason': 'Internal server error'} n_retries, backoff_factor = self.retries_params(self.media_type) log_path = f'http://{self.apple_pns.url_push}{self.path}' status_forcelist = tuple([status for status in range(500, 600)]) counter = 0 status = 500 reason = '' body = {} while counter <= n_retries: if self.connection: try: self.log_request(path=log_path) self.connection.request('POST', self.path, self.payload, self.headers) response = self.connection.get_response() reason_str = response.read().decode('utf8').replace("'", '"') if reason_str: reason_json = json.loads(reason_str) reason = reason_json.get('reason') else: reason = reason_str status = response.status if status not in status_forcelist: break except socket.gaierror: reason = 'socket error' except hyper.http20.exceptions.StreamResetError: reason = 'stream error' except ValueError as err: reason = f'Bad type of object in headers or payload: {err}' break else: reason = 'no connection' counter += 1 timer = backoff_factor * (2 ** (counter - 1)) time.sleep(timer) if counter == n_retries: reason = 'max retries reached' url = f'https:{self.connection.host}:{self.connection.port}{self.path}' if status != 200: details = self.apple_error_info(reason) if details: reason = f'{reason} - {details}' - + if status == 400 and 'BadDeviceToken' in reason: - status = 410 + status = 410 results = {'body': body, 'code': status, 'reason': reason, 'url': url, 'platform': 'apple', 'call_id': self.call_id, 'token': self.token } self.results = results self.log_results() return results def apple_error_info(self, reason): """ Give a human readable message according to 'reason' from apple APN. :returns : a string with message according to reason """ description_codes = {'ConnectionFailed': 'There was an error connecting to APNs.', 'InternalException': 'This exception should not be raised. If it is, please report this as a bug.', 'BadPayloadException': 'Something bad with the payload.', 'BadCollapseId': 'The collapse identifier exceeds the maximum allowed size', 'BadDeviceToken': 'The specified device token was bad. Verify that the request contains a valid token and that the token matches the environment.', 'BadExpirationDate:': 'The apns-expiration value is bad.', 'BadMessageId': 'The apns-id value is bad.', 'BadPriority': 'The apns-priority value is bad.', 'BadTopic': 'The apns-topic was invalid.', 'DeviceTokenNotForTopic': 'The device token does not match the specified topic.', 'DuplicateHeaders': 'One or more headers were repeated.', 'IdleTimeout': 'Idle time out.', 'MissingDeviceToken': 'The device token is not specified in the request :path. Verify that the :path header contains the device token.', 'MissingTopic': 'The apns-topic header of the request was not specified and was required. The apns-topic header is mandatory when the client is connected using a certificate that supports multiple topics.', 'PayloadEmpty': 'The message payload was empty.', 'TopicDisallowed': 'Pushing to this topic is not allowed.', 'BadCertificate': 'The certificate was bad.', 'BadCertificateEnvironment': 'The client certificate was for the wrong environment.', 'ExpiredProviderToken': 'The provider token is stale and a new token should be generated.', 'Forbidden': 'The specified action is not allowed.', 'InvalidProviderToken': 'The provider token is not valid or the token signature could not be verified.', 'MissingProviderToken': 'No provider certificate was used to connect to APNs and Authorization header was missing or no provider token was specified.', 'BadPath': 'The request contained a bad :path value.', 'MethodNotAllowed': 'The specified :method was not POST.', 'Unregistered': 'The device token is inactive for the specified topic.', 'PayloadTooLarge': 'The message payload was too large. The maximum payload size is 4096 bytes.', 'TooManyProviderTokenUpdates': 'The provider token is being updated too often.', 'TooManyRequests': 'Too many requests were made consecutively to the same device token.', 'InternalServerError': 'An internal server error occurred.', 'ServiceUnavailable': 'The service is unavailable.', 'Shutdown': 'The server is shutting down.', 'InvalidPushType': 'The apns-push-type value is invalid.'} try: message = description_codes[reason] return message except KeyError: return None diff --git a/pushserver/resources/utils.py b/pushserver/resources/utils.py index 766c106..dd1f019 100644 --- a/pushserver/resources/utils.py +++ b/pushserver/resources/utils.py @@ -1,376 +1,376 @@ import hashlib import json import logging import socket import ssl import time from ipaddress import ip_address from datetime import datetime __all__ = ['callid_to_uuid', 'fix_non_serializable_types', 'resources_available', 'ssl_cert', 'try_again', 'check_host', 'log_event', 'fix_device_id', 'fix_platform_name', 'log_incoming_request'] def callid_to_uuid(call_id: str) -> str: """ - + Generate a UUIDv4 from a callId. - + UUIDv4 format: five segments of seemingly random hex data, beginning with eight hex characters, followed by three four-character strings, then 12 characters at the end. These segments are separated by a “-”. :param call_id: `str` Globally unique identifier of a call. :return: a str with a uuidv4. """ hexa = hashlib.md5(call_id.encode()).hexdigest() uuidv4 = '%s-%s-%s-%s-%s' % \ (hexa[:8], hexa[8:12], hexa[12:16], hexa[16:20], hexa[20:]) return uuidv4 def fix_non_serializable_types(obj): """ Converts a non serializable object in an appropriate one, if it is possible and in a recursive way. If not, return the str 'No JSON Serializable object' :param obj: obj to convert """ if isinstance(obj, bytes): string = obj.decode() return fix_non_serializable_types(string) elif isinstance(obj, dict): return { fix_non_serializable_types(k): fix_non_serializable_types(v) for k, v in obj.items() } elif isinstance(obj, (tuple, list)): return [fix_non_serializable_types(elem) for elem in obj] elif isinstance(obj, str): try: dict_obj = json.loads(obj) return fix_non_serializable_types(dict_obj) except json.decoder.JSONDecodeError: return obj elif isinstance(obj, (bool, int, float)): return obj else: return def resources_available(host: str, port: int) -> bool: """ Check if a pair ip, port is available for a connection :param: `str` host :param: `int` port :return: a `bool` according to the test result. """ serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) if not host or not port: return None try: serversocket.bind((host, port)) serversocket.close() return True except OSError: return False def ssl_cert(cert_file: str, key_file=None) -> bool: """ Check if a ssl certificate is valid. :param cert_file: `str` path to certificate file :param key_file: `str` path to key file :return: `bool` True for a valid certificate. """ ssl_context = ssl.create_default_context() ssl_context.check_hostname = False ssl_context.verify_mode = ssl.CERT_NONE try: ssl_context.load_cert_chain(certfile=cert_file, keyfile=key_file) return True except (ssl.SSLError, NotADirectoryError, TypeError): return False def try_again(timer: int, host: str, port: int, start_error: str, loggers: dict) -> None: """ Sleep for a specific time and send log messages in case resources would not be available to start the app. :param timer: `int` time in seconds to wait (30 = DHCP delay) :param host: `str` IP address where app is trying to run :param port: `int` Host where app is trying to run :param start_error: `stṛ` Error msg to show in log. :param loggers: global logging instances to write messages (params.loggers) """ timer = timer # seconds, 30 for dhcp delay. level = 'error' msg = f"[can not init] on {host}:{port} - resources are not available" log_event(msg=start_error, level=level, loggers=loggers) log_event(msg=msg, level=level, loggers=loggers) msg = f'Server will try again in {timer} seconds' log_event(msg=msg, level=level, loggers=loggers) time.sleep(timer) def check_host(host, allowed_hosts) -> bool: """ Check if a host is in allowed_hosts :param host: `str` to check :return: `bool` """ if not allowed_hosts: return True for subnet in allowed_hosts: if ip_address(host) in subnet: return True return False def log_event(loggers: dict, msg: str, level: str = 'deb') -> None: """ Write log messages into log file and in journal if specified. :param loggers: `dict` global logging instances to write messages (params.loggers) :param msg: `str` message to write :param level: `str` info, error, deb or warn :param to_file: `bool` write just in file if True """ logger = loggers.get('to_journal') if logger.level != logging.DEBUG and loggers['debug'] is True: logger.setLevel(logging.DEBUG) elif logger.level != logging.INFO: logger.setLevel(logging.INFO) if level == 'info': logger.info(msg) elif level == 'error': logger.error(msg) elif level == 'warn': logger.warning(msg) elif level in ('deb', 'debug'): logger.debug(msg) def fix_device_id(device_id_to_fix: str) -> str: """ Remove special characters from uuid :param device_id_to_fix: `str` uuid with special characters. :return: a `str` with fixed uuid. """ if '>' in device_id_to_fix: uuid = device_id_to_fix.split(':')[-1].replace('>', '') elif ':' in device_id_to_fix: uuid = device_id_to_fix.split(':')[-1] else: uuid = device_id_to_fix device_id = uuid return device_id def fix_platform_name(platform: str) -> str: """ Fix platform name in case its value is 'android' or 'ios', replacing it for 'firebase' and 'apple' :param platform: `str` name of platform :return: a `str` with fixed name. """ if platform in ('firebase', 'android'): return 'firebase' elif platform in ('apple', 'ios'): return 'apple' else: return platform def fix_payload(body: dict) -> dict: payload = {} for item in body.keys(): value = body[item] if item in ('sip_to', 'sip_from'): item = item.split('_')[1] else: item = item.replace('_', '-') payload[item] = value return payload def pick_log_function(exc, *args, **kwargs): if ('rm_request' in exc.errors()[0]["loc"][1]): return log_remove_request(**kwargs) if ('add_request' in exc.errors()[0]["loc"][1]): return log_add_request(*args, **kwargs) else: return log_incoming_request(*args, **kwargs) def log_add_request(task: str, host: str, loggers: dict, - request_id: str = None, body: dict = None, - error_msg: str = None) -> None: + request_id: str = None, body: dict = None, + error_msg: str = None) -> None: """ Send log messages according to type of event. :param task: `str` type of event to log, can be 'log_request', 'log_success' or 'log_failure' :param host: `str` client host where request comes from :param loggers: `dict` global logging instances to write messages (params.loggers) :param request_id: `str` request ID generated on request :param body: `dict` body of request :param error_msg: `str` to show in log """ if task == 'log_request': payload = fix_payload(body) level = 'info' msg = f'{host} - Add Token - Request [{request_id}]: ' \ f'{payload}' log_event(msg=msg, level=level, loggers=loggers) elif task == 'log_success': payload = fix_payload(body) msg = f'{host} - Add Token - Response [{request_id}]: ' \ f'{payload}' level = 'info' log_event(msg=msg, level=level, loggers=loggers) elif task == 'log_failure': level = 'error' resp = error_msg msg = f'{host} - Add Token Failed - Response [{request_id}]: ' \ f'{resp}' log_event(loggers=loggers, msg=msg, level=level) def log_remove_request(task: str, host: str, loggers: dict, - request_id: str = None, body: dict = None, - error_msg: str = None) -> None: + request_id: str = None, body: dict = None, + error_msg: str = None) -> None: """ Send log messages according to type of event. :param task: `str` type of event to log, can be 'log_request', 'log_success' or 'log_failure' :param host: `str` client host where request comes from :param loggers: `dict` global logging instances to write messages (params.loggers) :param request_id: `str` request ID generated on request :param body: `dict` body of request :param error_msg: `str` to show in log """ if task == 'log_request': payload = fix_payload(body) level = 'info' msg = f'{host} - Remove Token - Request [{request_id}]: ' \ f'{payload}' log_event(msg=msg, level=level, loggers=loggers) elif task == 'log_success': payload = fix_payload(body) msg = f'{host} - Remove Token - Response [{request_id}]: ' \ f'{payload}' level = 'info' log_event(msg=msg, level=level, loggers=loggers) elif task == 'log_failure': level = 'error' resp = error_msg msg = f'{host} - Remove Token Failed - Response {request_id}: ' \ f'{resp}' log_event(loggers=loggers, msg=msg, level=level) def log_push_request(task: str, host: str, loggers: dict, request_id: str = None, body: dict = None, error_msg: str = None) -> None: """ Send log messages according to type of event. :param task: `str` type of event to log, can be 'log_request', 'log_success' or 'log_failure' :param host: `str` client host where request comes from :param loggers: `dict` global logging instances to write messages (params.loggers) :param request_id: `str` request ID generated on request :param body: `dict` body of request :param error_msg: `str` to show in log """ sip_to = body.get('sip_to') event = body.get('event') if task == 'log_request': payload = fix_payload(body) level = 'info' msg = f'{host} - Push - Request [{request_id}]: ' \ f'{event} for {sip_to} ' \ f': {payload}' log_event(msg=msg, level=level, loggers=loggers) elif task == 'log_failure': level = 'error' resp = error_msg msg = f'{host} - Push Failed - Response [{request_id}]: ' \ f'{resp}' log_event(loggers=loggers, msg=msg, level=level) def log_incoming_request(task: str, host: str, loggers: dict, request_id: str = None, body: dict = None, error_msg: str = None) -> None: """ Send log messages according to type of event. :param task: `str` type of event to log, can be 'log_request', 'log_success' or 'log_failure' :param host: `str` client host where request comes from :param loggers: `dict` global logging instances to write messages (params.loggers) :param request_id: `str` request ID generated on request :param body: `dict` body of request :param error_msg: `str` to show in log """ app_id = body.get('app_id') platform = body.get('platform') platform = platform if platform else '' sip_to = body.get('sip_to') device_id = body.get('device_id') device_id = fix_device_id(device_id) if device_id else None event = body.get('event') if task == 'log_request': payload = fix_payload(body) level = 'info' if sip_to: if device_id: msg = f'incoming {platform.title()} request {request_id}: ' \ f'{event} for {sip_to} using' \ f' device {device_id} from {host}: {payload}' else: msg = f'incoming {platform.title()} request {request_id}: ' \ f'{event} for {sip_to} ' \ f'from {host}: {payload}' elif device_id: msg = f'incoming {platform.title()} request {request_id}: ' \ f'{event} using' \ f' device {device_id} from {host}: {payload}' else: msg = f'incoming {platform.title()} request {request_id}: ' \ f' from {host}: {payload}' log_event(msg=msg, level=level, loggers=loggers) elif task == 'log_success': msg = f'incoming {platform.title()} response for {request_id}: ' \ f'push accepted' level = 'info' log_event(msg=msg, level=level, loggers=loggers) elif task == 'log_failure': level = 'error' resp = error_msg msg = f'incoming {platform.title()} from {host} response for {request_id}, ' \ f'push rejected: {resp}' log_event(loggers=loggers, msg=msg, level=level) diff --git a/scripts/sylk-pushclient-v2 b/scripts/sylk-pushclient-v2 index 103e5e8..8f23f13 100755 --- a/scripts/sylk-pushclient-v2 +++ b/scripts/sylk-pushclient-v2 @@ -1,134 +1,133 @@ #!/usr/bin/python -import json -import logging +# import json +# import logging import re import sys import requests -try: - import pymysql -except ImportError: - pass +# try: +# import pymysql +# except ImportError: +# pass from argparse import ArgumentParser if __name__ == '__main__': parser = ArgumentParser() subparsers = parser.add_subparsers(dest='action') parser.add_argument('--url', dest='url', required=False, default='http://localhost:8400', help='Base push URL') parser.add_argument('--account', dest='account', required=True, help='Account') - + subparserA = subparsers.add_parser('push', help='Send push request') subparserA.add_argument('--mediatype', dest='media_type', default="audio", required=False, help='Audio, Video or Message') subparserA.add_argument('--callid', dest='call_id', required=True, help='Call ID') subparserA.add_argument('--event', dest='event', required=False, help='Event', default='incoming_session') subparserA.add_argument('--from', dest='from_uri', required=True, help='From') subparserA.add_argument('--from_name', dest='from_name', required=False, help='From name') subparserA.add_argument('--to', dest='to_uri', required=True, help='To') subparserA.add_argument('--reason', dest='reason', required=False, help='Reason') subparserA.add_argument('--badge', dest='badge', default=None, required=False, help='Badge to display') subparserA.add_argument('--deviceid', dest='device_id', default=None, required=False, help='Device Id/Sip instance') subparserB = subparsers.add_parser('add', help='Add a push token') subparserB.add_argument('--platform', dest='platform', help='Platform') subparserB.add_argument('--appid', dest='appid', required=True, help='App ID') subparserB.add_argument('--token', dest='device_token', required=True, help='Device token') subparserB.add_argument('--deviceid', dest='device_id', required=True, help='Device Id') subparserB.add_argument('--silent', dest='silent', default="1", required=False, help='Silent') subparserB.add_argument('--user_agent', dest='user_agent', default="None", required=False, help='User Agent') subparserC = subparsers.add_parser('remove', help='Remove a push token') subparserC.add_argument('--appid', dest='appid', required=True, help='App ID') subparserC.add_argument('--deviceid', dest='device_id', required=True, help='Device Id') options = parser.parse_args() try: from_uri = re.sub(r'^"|"$', '', options.from_uri) except AttributeError: pass try: from_name = options.from_name.strip('\"') if options.from_name else None except AttributeError: pass try: media_type = options.media_type if ("video" in options.media_type): media_type = 'video' elif ("audio" in options.media_type): media_type = 'audio' except AttributeError: pass - if options.url[-1] == '/': - options.url=options.url[:-1] + options.url = options.url[:-1] url = '{}/{}/{}'.format(options.url, 'v2/tokens', options.account) if options.action == 'add': log_params = {'platform': options.platform, 'app-id': options.appid, 'token': options.device_token, 'device-id': options.device_id, 'silent': options.silent, 'user-agent': options.user_agent} elif options.action == 'remove': log_params = {'app-id': options.appid, 'device-id': options.device_id} else: log_params = {'media-type': media_type, 'event': options.event, 'from': from_uri, 'from-display-name': from_name or from_uri, 'to': options.to_uri, 'call-id': options.call_id, 'badge': options.badge, 'reason': options.reason} if options.device_id is None: url = '{}/{}/{}/push'.format(options.url, 'v2/tokens', options.account) else: url = '{}/{}/{}/push/{}'.format(options.url, 'v2/tokens', options.account, options.device_id) def getMethod(*args, **kwargs): if options.action == 'remove': return requests.delete(*args, **kwargs) else: return requests.post(*args, **kwargs) action = options.action.title() try: r = getMethod(url, timeout=5, json=log_params) print("%s request to %s - %s: %s" % (action, url, r.status_code, r.text)) if r.status_code >= 200 and r.status_code < 300: sys.exit(0) elif r.status_code == 410: body = r.json() try: for result in body['data']: failure = result['body']['_content']['failure'] if failure == 1: # A push client may want to act based on various response codes # https://firebase.google.com/docs/cloud-messaging/http-server-ref#error-codes reason = result['body']['_content']['results'][0]['error'] if reason == 'NotRegistered': - log.info("Token %s must be purged" % token) - #q = "delete from push_tokens where token = '%s'" % token - #con = pymysql.connect('localhost', 'opensips', 'XYZ', 'opensips') - #with con: + print("Token %s must be purged" % token) + # q = "delete from push_tokens where token = '%s'" % token + # con = pymysql.connect('localhost', 'opensips', 'XYZ', 'opensips') + # with con: # cur = con.cursor() # cur.execute(q) except KeyError: pass sys.exit(0) else: print("%s request to %s failed: %d: %s" % (action, url, r.status_code, r.text)) sys.exit(1) except Exception as e: print("%s request to %s failed: connection error" % (action, url)) sys.exit(1)