Manual merge v2 notifications encryption

This commit is contained in:
JonnyWong16 2017-04-19 19:58:39 -07:00
commit 59f12c71a9
11 changed files with 11616 additions and 84 deletions

View file

@ -481,7 +481,7 @@ def dbcheck():
'CREATE TABLE IF NOT EXISTS notify_log (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER, '
'session_key INTEGER, rating_key INTEGER, parent_rating_key INTEGER, grandparent_rating_key INTEGER, '
'user_id INTEGER, user TEXT, notifier_id INTEGER, agent_id INTEGER, agent_name TEXT, notify_action TEXT, '
'subject_text TEXT, body_text TEXT, script_args TEXT)'
'subject_text TEXT, body_text TEXT, script_args TEXT, success INTEGER DEFAULT 0)'
)
# library_sections table :: This table keeps record of the servers library sections
@ -958,6 +958,18 @@ def dbcheck():
'ALTER TABLE notify_log ADD COLUMN notifier_id INTEGER'
)
# Upgrade notify_log table from earlier versions
try:
c_db.execute('SELECT send_success FROM notify_log')
except sqlite3.OperationalError:
logger.debug(u"Altering database. Updating database table notify_log.")
c_db.execute(
'ALTER TABLE notify_log ADD COLUMN success INTEGER DEFAULT 0'
)
c_db.execute(
'UPDATE notify_log SET success = 1'
)
# Upgrade library_sections table from earlier versions (remove UNIQUE constraint on section_id)
try:
result = c_db.execute('SELECT SQL FROM sqlite_master WHERE type="table" AND name="library_sections"').fetchone()

View file

@ -1321,7 +1321,8 @@ class DataFactory(object):
'notify_log.agent_name',
'notify_log.notify_action',
'notify_log.subject_text',
'notify_log.body_text'
'notify_log.body_text',
'notify_log.success'
]
try:
query = data_tables.ssp_query(table_name='notify_log',
@ -1360,7 +1361,8 @@ class DataFactory(object):
'agent_name': item['agent_name'],
'notify_action': item['notify_action'],
'subject_text': item['subject_text'],
'body_text': body_text
'body_text': body_text,
'success': item['success']
}
rows.append(row)

View file

@ -190,32 +190,43 @@ def notify(notifier_id=None, notify_action=None, stream_data=None, timeline_data
if not notifier_config:
return
# Get the subject and body strings
subject_string = notifier_config['notify_text'][notify_action]['subject']
body_string = notifier_config['notify_text'][notify_action]['body']
if notify_action == 'test':
subject = kwargs.pop('subject', 'PlexPy')
body = kwargs.pop('body', 'Test Notification')
script_args = kwargs.pop('script_args', [])
else:
# Get the subject and body strings
subject_string = notifier_config['notify_text'][notify_action]['subject']
body_string = notifier_config['notify_text'][notify_action]['body']
# Format the subject and body strings
subject, body, script_args = build_notify_text(subject=subject_string,
body=body_string,
notify_action=notify_action,
parameters=parameters,
agent_id=notifier_config['agent_id'])
# Send the notification
notifiers.send_notification(notifier_id=notifier_config['id'],
subject=subject,
body=body,
script_args=script_args,
notify_action=notify_action,
parameters=parameters)
# Format the subject and body strings
subject, body, script_args = build_notify_text(subject=subject_string,
body=body_string,
notify_action=notify_action,
parameters=parameters,
agent_id=notifier_config['agent_id'])
# Set the notification state in the db
set_notify_state(session=stream_data or timeline_data,
notify_action=notify_action,
notifier=notifier_config,
subject=subject,
body=body,
script_args=script_args)
notification_id = set_notify_state(session=stream_data or timeline_data,
notify_action=notify_action,
notifier=notifier_config,
subject=subject,
body=body,
script_args=script_args)
# Send the notification
success = notifiers.send_notification(notifier_id=notifier_config['id'],
subject=subject,
body=body,
script_args=script_args,
notify_action=notify_action,
notification_id=notification_id,
parameters=parameters,
**kwargs)
if success:
set_notify_success(notification_id)
return True
def get_notify_state(session):
@ -263,10 +274,19 @@ def set_notify_state(notify_action, notifier, subject, body, script_args, sessio
'script_args': script_args}
monitor_db.upsert(table_name='notify_log', key_dict=keys, value_dict=values)
return monitor_db.last_insert_id()
else:
logger.error(u"PlexPy NotificationHandler :: Unable to set notify state.")
def set_notify_success(notification_id):
keys = {'id': notification_id}
values = {'success': 1}
monitor_db = database.MonitorDatabase()
monitor_db.upsert(table_name='notify_log', key_dict=keys, value_dict=values)
def build_media_notify_params(notify_action=None, session=None, timeline=None, **kwargs):
# Get time formats
date_format = plexpy.CONFIG.DATE_FORMAT.replace('Do','')

View file

@ -35,6 +35,22 @@ import urllib2
from urlparse import urlparse
import uuid
try:
from Cryptodome.Protocol.KDF import PBKDF2
from Cryptodome.Cipher import AES
from Cryptodome.Random import get_random_bytes
from Cryptodome.Hash import HMAC, SHA1
CRYPTODOME = True
except ImportError:
try:
from Crypto.Protocol.KDF import PBKDF2
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Hash import HMAC, SHA1
CRYPTODOME = True
except ImportError:
CRYPTODOME = False
import gntp.notifier
import facebook
import twitter
@ -49,6 +65,7 @@ import request
from plexpy.config import _BLACKLIST_KEYS, _WHITELIST_KEYS
from plexpy.helpers import checked
AGENT_IDS = {'growl': 0,
'prowl': 1,
'xbmc': 2,
@ -524,7 +541,7 @@ def set_notifier_config(notifier_id=None, agent_id=None, **kwargs):
return False
def send_notification(notifier_id=None, subject='', body='', notify_action='', **kwargs):
def send_notification(notifier_id=None, subject='', body='', notify_action='', notification_id=None, **kwargs):
notifier_config = get_notifier_config(notifier_id=notifier_id)
if notifier_config:
agent = get_agent_class(agent_id=notifier_config['agent_id'],
@ -532,6 +549,7 @@ def send_notification(notifier_id=None, subject='', body='', notify_action='', *
return agent.notify(subject=subject,
body=body,
action=notify_action.split('on_')[-1],
notification_id=notification_id,
**kwargs)
else:
logger.debug(u"PlexPy Notifiers :: Notification requested but no notifier_id received.")
@ -671,12 +689,13 @@ class ANDROIDAPP(Notifier):
"""
PlexPy Android app notifications
"""
_DEFAULT_CONFIG = {'device_id': ''
_DEFAULT_CONFIG = {'device_id': '',
'priority': 3
}
ONESIGNAL_APP_ID = '3b4b666a-d557-4b92-acdf-e2c8c4b95357'
def notify(self, subject='', body='', action='', **kwargs):
def notify(self, subject='', body='', action='', notification_id=None, **kwargs):
if not subject or not body:
return
@ -685,25 +704,69 @@ class ANDROIDAPP(Notifier):
logger.warn(u"PlexPy Notifiers :: Unable to send Android app notification: device not registered.")
return
data = {'app_id': self.ONESIGNAL_APP_ID,
'include_player_ids': [self.config['device_id']],
'headings': {'en': subject.encode("utf-8")},
'contents': {'en': body.encode("utf-8")}
}
plaintext_data = {'notification_id': notification_id,
'subject': subject.encode("utf-8"),
'body': body.encode("utf-8"),
'priority': self.config['priority']}
http_handler = HTTPSConnection("onesignal.com")
http_handler.request("POST",
"/api/v1/notifications",
headers={'Content-type': "application/json"},
body=json.dumps(data))
response = http_handler.getresponse()
request_status = response.status
#logger.debug("Plaintext data: {}".format(plaintext_data))
if CRYPTODOME:
# Key generation
salt = get_random_bytes(16)
passphrase = plexpy.CONFIG.API_KEY
key_length = 32 # AES256
iterations = 1000
key = PBKDF2(passphrase, salt, dkLen=key_length, count=iterations,
prf=lambda p, s: HMAC.new(p, s, SHA1).digest())
#logger.debug("Encryption key (base64): {}".format(base64.b64encode(key)))
# Encrypt using AES GCM
nonce = get_random_bytes(16)
cipher = AES.new(key, AES.MODE_GCM, nonce)
encrypted_data, gcm_tag = cipher.encrypt_and_digest(json.dumps(plaintext_data))
encrypted_data += gcm_tag
#logger.debug("Encrypted data (base64): {}".format(base64.b64encode(encrypted_data)))
#logger.debug("GCM tag (base64): {}".format(base64.b64encode(gcm_tag)))
#logger.debug("Nonce (base64): {}".format(base64.b64encode(nonce)))
#logger.debug("Salt (base64): {}".format(base64.b64encode(salt)))
payload = {'app_id': self.ONESIGNAL_APP_ID,
'include_player_ids': [self.config['device_id']],
'contents': {'en': 'PlexPy Notification'},
'data': {'encrypted': True,
'cipher_text': base64.b64encode(encrypted_data),
'nonce': base64.b64encode(nonce),
'salt': base64.b64encode(salt)}
}
else:
logger.warn(u"PlexPy Notifiers :: PyCryptodome library is missing. "
"Android app notifications will be sent unecrypted. "
"Install the library to encrypt the notifications.")
payload = {'app_id': self.ONESIGNAL_APP_ID,
'include_player_ids': [self.config['device_id']],
'contents': {'en': 'PlexPy Notification'},
'data': {'encrypted': False,
'plain_text': plaintext_data}
}
#logger.debug("OneSignal payload: {}".format(payload))
headers = {'Content-Type': 'application/json'}
r = requests.post("https://onesignal.com/api/v1/notifications", headers=headers, json=payload)
request_status = r.status_code
#logger.debug("OneSignal response: {}".format(r.content))
if request_status == 200:
logger.info(u"PlexPy Notifiers :: Android app notification sent.")
return True
elif request_status >= 400 and request_status < 500:
logger.warn(u"PlexPy Notifiers :: Android app notification failed: [%s] %s" % (request_status, response.reason))
logger.warn(u"PlexPy Notifiers :: Android app notification failed: [%s] %s" % (request_status, r.reason))
return False
else:
logger.warn(u"PlexPy Notifiers :: Android app notification failed.")
@ -729,27 +792,62 @@ class ANDROIDAPP(Notifier):
return devices
def return_config_options(self):
config_option = []
if not CRYPTODOME:
config_option.append({
'label': 'Warning',
'description': '<strong>The PyCryptodome library is missing. ' \
'The content of your notifications will be sent unencrypted!</strong><br>' \
'Please install the library to encrypt the notification contents. ' \
'Instructions can be found in the ' \
'<a href="' + helpers.anon_url('https://github.com/%s/plexpy/wiki/Frequently-Asked-Questions-(FAQ)#notifications-pycryptodome' % plexpy.CONFIG.GIT_USER) + '" target="_blank">FAQ</a>.',
'input_type': 'help'
})
else:
config_option.append({
'label': 'Note',
'description': 'The PyCryptodome library was found. ' \
'The content of your notifications will be sent encrypted!',
'input_type': 'help'
})
config_option[-1]['description'] += '<br><br>Notifications are sent using the ' \
'<a href="' + helpers.anon_url('https://onesignal.com') + '" target="_blank">' \
'OneSignal</a> API. Some user data is collected and cannot be encrypted. ' \
'Please read the <a href="' + helpers.anon_url('https://onesignal.com/privacy_policy') + '" target="_blank">' \
'OneSignal Privacy Policy</a> for more details.'
devices = self.get_devices()
if not devices:
devices_config = {'label': 'Device',
'description': 'No devices registered. ' \
'<a data-tab-destination="tabs-android_app" data-toggle="tab" data-dismiss="modal" ' \
'style="cursor: pointer;">Get the Android App</a> and register a device.',
'input_type': 'help'
}
config_option.append({
'label': 'Device',
'description': 'No devices registered. ' \
'<a data-tab-destination="tabs-android_app" data-toggle="tab" data-dismiss="modal" ' \
'style="cursor: pointer;">Get the Android App</a> and register a device.',
'input_type': 'help'
})
else:
devices_config = {'label': 'Device',
'value': self.config['device_id'],
'name': 'androidapp_device_id',
'description': 'Set your Android app device or ' \
'<a data-tab-destination="tabs-android_app" data-toggle="tab" data-dismiss="modal" ' \
'style="cursor: pointer;">register a new device</a> with PlexPy.',
'input_type': 'select',
'select_options': devices
}
config_option.append({
'label': 'Device',
'value': self.config['device_id'],
'name': 'androidapp_device_id',
'description': 'Set your Android app device or ' \
'<a data-tab-destination="tabs-android_app" data-toggle="tab" data-dismiss="modal" ' \
'style="cursor: pointer;">register a new device</a> with PlexPy.',
'input_type': 'select',
'select_options': devices
})
config_option = [devices_config]
config_option.append({
'label': 'Priority',
'value': self.config['priority'],
'name': 'androidapp_priority',
'description': 'Set the notification priority.',
'input_type': 'select',
'select_options': {1: 'Minimum', 2: 'Low', 3: 'Normal', 4: 'High'}
})
return config_option
@ -1006,23 +1104,23 @@ class DISCORD(Notifier):
'description': 'Your Discord incoming webhook URL.',
'input_type': 'text'
},
{'label': 'Discord Username',
'value': self.config['username'],
'name': 'discord_username',
'description': 'The Discord username which will be used. Leave blank for webhook integration default.',
'input_type': 'text'
{'label': 'Discord Username',
'value': self.config['username'],
'name': 'discord_username',
'description': 'The Discord username which will be used. Leave blank for webhook integration default.',
'input_type': 'text'
},
{'label': 'Discord Avatar',
'value': self.config['avatar_url'],
'description': 'The image url for the avatar which will be used. Leave blank for webhook integration default.',
'name': 'discord_avatar_url',
'input_type': 'text'
{'label': 'Discord Avatar',
'value': self.config['avatar_url'],
'description': 'The image url for the avatar which will be used. Leave blank for webhook integration default.',
'name': 'discord_avatar_url',
'input_type': 'text'
},
{'label': 'Discord Color',
'value': self.config['color'],
'description': 'The hex color value (starting with \'#\') for the border along the left side of the message attachment.',
'name': 'discord_color',
'input_type': 'text'
{'label': 'Discord Color',
'value': self.config['color'],
'description': 'The hex color value (starting with \'#\') for the border along the left side of the message attachment.',
'name': 'discord_color',
'input_type': 'text'
},
{'label': 'TTS',
'value': self.config['tts'],
@ -1843,7 +1941,7 @@ class NMA(Notifier):
{'label': 'Priority',
'value': self.config['priority'],
'name': 'nma_priority',
'description': 'Set the priority.',
'description': 'Set the notification priority.',
'input_type': 'select',
'select_options': {-2: -2, -1: -1, 0: 0, 1: 1, 2: 2}
}
@ -2104,7 +2202,7 @@ class PROWL(Notifier):
{'label': 'Priority',
'value': self.config['priority'],
'name': 'prowl_priority',
'description': 'Set the priority.',
'description': 'Set the notification priority.',
'input_type': 'select',
'select_options': {-2: -2, -1: -1, 0: 0, 1: 1, 2: 2}
}
@ -2354,7 +2452,7 @@ class PUSHOVER(Notifier):
{'label': 'Priority',
'value': self.config['priority'],
'name': 'pushover_priority',
'description': 'Set the priority.',
'description': 'Set the notification priority.',
'input_type': 'select',
'select_options': {-2: -2, -1: -1, 0: 0, 1: 1, 2: 2}
},

View file

@ -3083,10 +3083,10 @@ class WebInterface(object):
if notifier:
logger.debug(u"Sending %s%s notification." % (test, notifier['agent_name']))
if notifiers.send_notification(notifier_id=notifier_id,
if notification_handler.notify(notifier_id=notifier_id,
notify_action=notify_action,
subject=subject,
body=body,
notify_action=notify_action,
**kwargs):
return "Notification sent."
else: