Commit 4b03268b authored by Tijmen de Mes's avatar Tijmen de Mes

Handle replicated SIP messages

parent b9b6e2df
......@@ -71,6 +71,10 @@ class SMSSettings(SettingsGroup):
private_key = Setting(type=ApplicationDataPath, default=None, nillable=True)
class SMSSettingsExtension(SMSSettings):
enable_message_replication = Setting(type=bool, default=True)
class SoundSettings(SettingsGroup):
inbound_ringtone = Setting(type=SoundFile, default=None, nillable=True)
......@@ -89,7 +93,7 @@ class AccountExtension(SettingsObjectExtension):
rtp = RTPSettingsExtension
server = ServerSettings
sip = SIPSettingsExtension
sms = SMSSettings
sms = SMSSettingsExtension
sounds = SoundSettings
xcap = XCAPSettingsExtension
......
......@@ -671,6 +671,7 @@ class MessageManager(object, metaclass=Singleton):
if account is None:
return
log.info(f'Received a message for {account.id}')
data = notification.data
......@@ -679,150 +680,176 @@ class MessageManager(object, metaclass=Singleton):
x_replicated_message = data.headers.get('X-Replicated-Message', Null)
to_header = data.headers.get('To', Null)
if x_replicated_message is Null:
cpim_message = None
if content_type == "message/cpim":
try:
cpim_message = CPIMPayload.decode(data.body)
except CPIMParserError:
log.warning('SIP message from %s to %s rejected: CPIM parse error' % (from_header.uri, '%s@%s' % (to_header.uri.user, to_header.uri.host)))
return
body = cpim_message.content if isinstance(cpim_message.content, str) else cpim_message.content.decode()
content_type = cpim_message.content_type
sender = cpim_message.sender or from_header
disposition = next(([item.strip() for item in header.value.split(',')] for header in cpim_message.additional_headers if header.name == 'Disposition-Notification'), None)
message_id = next((header.value for header in cpim_message.additional_headers if header.name == 'Message-ID'), str(uuid.uuid4()))
else:
payload = SimplePayload.decode(data.body, data.content_type)
body = payload.content.decode()
content_type = payload.content_type
sender = from_header
disposition = None
message_id = str(uuid.uuid4())
encryption = self.check_encryption(content_type, body)
if encryption == 'OpenPGP':
log.info('Message is Open PGP encrypted')
if account.sms.enable_pgp and (account.sms.private_key is None or not os.path.exists(account.sms.private_key.normalized)):
if not self.pgp_requests[account, GeneratePGPKeyRequest]:
generate_dialog = GeneratePGPKeyDialog()
generate_request = GeneratePGPKeyRequest(generate_dialog, account, 0)
generate_request.accepted.connect(self._SH_GeneratePGPKeys)
generate_request.finished.connect(self._SH_PGPRequestFinished)
bisect.insort_right(self.pgp_requests, generate_request)
generate_request.dialog.show()
elif not account.sms.enable_pgp:
log.info(f"-- Skipping PGP encrypted message, PGP is disabled for {account.id}")
return
if x_replicated_message is not Null:
log.info('Message is a replicated message')
if not account.sms.enable_message_replication:
log.info('Skipping message, replicated message handling is disabled')
return
if content_type.lower() == 'text/pgp-private-key':
log.info('Message is a private key')
if not account.sms.enable_pgp:
log.info(f"-- Skipping private key import, PGP is disabled for {account.id}")
return
regex = "(?P<public_key>-----BEGIN PGP PUBLIC KEY BLOCK-----.*-----END PGP PUBLIC KEY BLOCK-----)"
matches = re.search(regex, body, re.DOTALL)
public_key = matches.group('public_key')
cpim_message = None
if content_type == "message/cpim":
try:
cpim_message = CPIMPayload.decode(data.body)
except CPIMParserError:
log.warning('SIP message from %s to %s rejected: CPIM parse error' % (from_header.uri, '%s@%s' % (to_header.uri.user, to_header.uri.host)))
return
body = cpim_message.content if isinstance(cpim_message.content, str) else cpim_message.content.decode()
content_type = cpim_message.content_type
sender = cpim_message.sender or from_header
disposition = next(([item.strip() for item in header.value.split(',')] for header in cpim_message.additional_headers if header.name == 'Disposition-Notification'), None)
message_id = next((header.value for header in cpim_message.additional_headers if header.name == 'Message-ID'), str(uuid.uuid4()))
else:
payload = SimplePayload.decode(data.body, data.content_type)
body = payload.content.decode()
content_type = payload.content_type
sender = from_header
disposition = None
message_id = str(uuid.uuid4())
encryption = self.check_encryption(content_type, body)
if encryption == 'OpenPGP':
log.info('Message is Open PGP encrypted')
if account.sms.enable_pgp and (account.sms.private_key is None or not os.path.exists(account.sms.private_key.normalized)):
if not self.pgp_requests[account, GeneratePGPKeyRequest]:
generate_dialog = GeneratePGPKeyDialog()
generate_request = GeneratePGPKeyRequest(generate_dialog, account, 0)
generate_request.accepted.connect(self._SH_GeneratePGPKeys)
generate_request.finished.connect(self._SH_PGPRequestFinished)
bisect.insort_right(self.pgp_requests, generate_request)
generate_request.dialog.show()
elif not account.sms.enable_pgp:
log.info(f"-- Skipping PGP encrypted message, PGP is disabled for {account.id}")
return
return
if self._compare_public_key(account, public_key):
return
for request in self.pgp_requests[account]:
request.dialog.hide()
self.pgp_requests.remove(request)
if content_type.lower() == 'text/pgp-private-key':
log.info('Message is a private key')
if not account.sms.enable_pgp:
log.info(f"-- Skipping private key import, PGP is disabled for {account.id}")
return
regex = "(?P<public_key>-----BEGIN PGP PUBLIC KEY BLOCK-----.*-----END PGP PUBLIC KEY BLOCK-----)"
matches = re.search(regex, body, re.DOTALL)
public_key = matches.group('public_key')
if self._compare_public_key(account, public_key):
return
import_dialog = ImportDialog()
incoming_request = ImportPrivateKeyRequest(import_dialog, body, account)
incoming_request.accepted.connect(self._SH_ImportPGPKeys)
incoming_request.finished.connect(self._SH_PGPRequestFinished)
bisect.insort_right(self.pgp_requests, incoming_request)
incoming_request.dialog.show()
for request in self.pgp_requests[account]:
request.dialog.hide()
self.pgp_requests.remove(request)
if content_type.lower() == 'text/pgp-public-key':
log.info('Message is a public key')
self._save_pgp_key(body, sender.uri)
import_dialog = ImportDialog()
incoming_request = ImportPrivateKeyRequest(import_dialog, body, account)
incoming_request.accepted.connect(self._SH_ImportPGPKeys)
incoming_request.finished.connect(self._SH_PGPRequestFinished)
bisect.insort_right(self.pgp_requests, incoming_request)
incoming_request.dialog.show()
from blink.contacts import URIUtils
contact, contact_uri = URIUtils.find_contact(sender.uri)
session_manager = SessionManager()
if content_type.lower() == 'text/pgp-public-key':
log.info('Message is a public key')
self._save_pgp_key(body, sender.uri)
notification_center = NotificationCenter()
from blink.contacts import URIUtils
contact, contact_uri = URIUtils.find_contact(sender.uri)
if x_replicated_message is not Null:
contact, contact_uri = URIUtils.find_contact(to_header.uri)
timestamp = str(cpim_message.timestamp) if cpim_message is not None and cpim_message.timestamp is not None else str(ISOTimestamp.now())
message = BlinkMessage(body, content_type, sender, timestamp=timestamp, id=message_id, disposition=disposition, direction='incoming')
session_manager = SessionManager()
notification_center = NotificationCenter()
timestamp = str(cpim_message.timestamp) if cpim_message is not None and cpim_message.timestamp is not None else str(ISOTimestamp.now())
message = BlinkMessage(body, content_type, sender, timestamp=timestamp, id=message_id, disposition=disposition, direction='incoming')
if x_replicated_message is not Null:
message.sender = account
message.direction = "outgoing"
try:
blink_session = next(session for session in self.sessions if session.contact.settings is contact.settings)
except StopIteration:
blink_session = None
if content_type.lower() in self.__ignored_content_types__:
log.debug(f"Not creating session for incoming message for content type {content_type.lower()}")
if content_type.lower() != IMDNDocument.content_type:
return
elif x_replicated_message is not Null:
log.debug("Not creating session for incoming message, message is replicated")
notification_center.post_notification('BlinkGotHistoryMessage',
sender=account,
data=NotificationData(remote_uri=contact.uri.uri,
message=message,
encryption=encryption,
state='accepted'))
return
else:
log.debug("Starting new message session for incoming message")
blink_session = session_manager.create_session(contact, contact_uri, [StreamDescription('messages')], account=account, connect=False)
else:
if blink_session.fake_streams.get('messages') is None:
stream = StreamDescription('messages')
blink_session.fake_streams.extend([stream.create_stream()])
blink_session._delete_when_done = False
if account.sms.enable_pgp and account.sms.private_key is not None and os.path.exists(account.sms.private_key.normalized):
blink_session.fake_streams.get('messages').enable_pgp()
notification_center.post_notification('BlinkSessionWillAddStream', sender=blink_session, data=NotificationData(stream=stream))
if account.sms.use_cpim and account.sms.enable_imdn and content_type.lower() == IMDNDocument.content_type:
# print("-- IMDN received")
document = IMDNDocument.parse(body)
imdn_message_id = document.message_id.value
imdn_status = document.notification.status.__str__()
imdn_datetime = document.datetime.__str__()
notification_center.post_notification('BlinkGotDispositionNotification', sender=blink_session, data=NotificationData(id=imdn_message_id, status=imdn_status))
return
elif content_type.lower() == IMDNDocument.content_type:
# print("-- IMDN received, ignored")
return
if content_type.lower() in ['text/pgp-public-key', 'text/pgp-private-key']:
notification_center.post_notification('PGPKeysShouldReload', sender=blink_session)
return
if content_type.lower() == IsComposingDocument.content_type:
try:
blink_session = next(session for session in self.sessions if session.contact.settings is contact.settings)
except StopIteration:
blink_session = None
if content_type.lower() in self.__ignored_content_types__:
log.debug(f"Not creating session for incoming message for content type {content_type.lower()}")
if content_type.lower() != IMDNDocument.content_type:
return
else:
log.debug("Starting new message session for incoming message")
blink_session = session_manager.create_session(contact, contact_uri, [StreamDescription('messages')], account=account, connect=False)
document = IsComposingMessage.parse(body)
except ParserError as e:
log.warning('Failed to parse Is-Composing payload: %s' % str(e))
else:
if blink_session.fake_streams.get('messages') is None:
stream = StreamDescription('messages')
blink_session.fake_streams.extend([stream.create_stream()])
blink_session._delete_when_done = False
if account.sms.enable_pgp and account.sms.private_key is not None and os.path.exists(account.sms.private_key.normalized):
blink_session.fake_streams.get('messages').enable_pgp()
notification_center.post_notification('BlinkSessionWillAddStream', sender=blink_session, data=NotificationData(stream=stream))
if account.sms.use_cpim and account.sms.enable_imdn and content_type.lower() == IMDNDocument.content_type:
document = IMDNDocument.parse(body)
imdn_message_id = document.message_id.value
imdn_status = document.notification.status.__str__()
imdn_datetime = document.datetime.__str__()
notification_center.post_notification('BlinkGotDispositionNotification', sender=blink_session, data=NotificationData(id=imdn_message_id, status=imdn_status))
return
elif content_type.lower() == IMDNDocument.content_type:
return
if content_type.lower() in ['text/pgp-public-key', 'text/pgp-private-key']:
notification_center.post_notification('PGPKeysShouldReload', sender=blink_session)
return
data = NotificationData(state=document.state.value,
refresh=document.refresh.value if document.refresh is not None else 120,
content_type=document.content_type.value if document.content_type is not None else None,
last_active=document.last_active.value if document.last_active is not None else None,
sender=sender)
notification_center.post_notification('BlinkGotComposingIndication', sender=blink_session, data=data)
return
if content_type.lower() == IsComposingDocument.content_type:
try:
document = IsComposingMessage.parse(body)
except ParserError as e:
log.warning('Failed to parse Is-Composing payload: %s' % str(e))
else:
data = NotificationData(state=document.state.value,
refresh=document.refresh.value if document.refresh is not None else 120,
content_type=document.content_type.value if document.content_type is not None else None,
last_active=document.last_active.value if document.last_active is not None else None,
sender=sender)
notification_center.post_notification('BlinkGotComposingIndication', sender=blink_session, data=data)
return
if not content_type.lower().startswith('text'):
return
if not content_type.lower().startswith('text'):
return
if x_replicated_message or account is not blink_session.account:
history_message_data = NotificationData(remote_uri=contact.uri.uri,
message=message,
encryption=encryption,
state='accepted')
notification_center.post_notification('BlinkGotHistoryMessage', sender=account, data=history_message_data)
if account is not blink_session.account:
history_message_data = NotificationData(remote_uri=contact.uri.uri,
message=message,
encryption=encryption,
state='accepted')
notification_center.post_notification('BlinkGotHistoryMessage', sender=account, data=history_message_data)
if encryption == 'OpenPGP':
if blink_session.fake_streams.get('messages').can_decrypt:
blink_session.fake_streams.get('messages').decrypt(message)
else:
self._incoming_encrypted_message_queue.append((message, account, contact))
if account is blink_session.account:
notification_center.post_notification('BlinkMessageIsParsed', sender=blink_session, data=message)
self._add_contact_to_messages_group(blink_session.account, blink_session.contact)
notification_center.post_notification('BlinkGotMessage', sender=blink_session, data=message)
return
if encryption == 'OpenPGP':
if blink_session.fake_streams.get('messages').can_decrypt:
blink_session.fake_streams.get('messages').decrypt(message)
else:
self._incoming_encrypted_message_queue.append((message, account, contact))
if account is blink_session.account:
notification_center.post_notification('BlinkMessageIsParsed', sender=blink_session, data=message)
self._add_contact_to_messages_group(blink_session.account, blink_session.contact)
notification_center.post_notification('BlinkGotMessage',
sender=blink_session,
data=message)
return
self._handle_incoming_message(message, blink_session, account)
else:
# TODO handle replicated messages
pass
self._handle_incoming_message(message, blink_session, account)
def _NH_BlinkSessionWasCreated(self, notification):
session = notification.sender
......
......@@ -262,6 +262,7 @@ class PreferencesWindow(base_class, ui_class, metaclass=QSingleton):
self.message_imdn_enabled_button.clicked.connect(self._SH_EnableMessageIMDNButtonClicked)
self.message_add_unknown_contacts_button.clicked.connect(self._SH_AddUnknownContactsButtonClicked)
self.message_pgp_enabled_button.clicked.connect(self._SH_EnablePGPButtonClicked)
self.message_replication_button.clicked.connect(self._SH_MessageReplicationButtonClicked)
# Audio devices
self.audio_alert_device_button.activated[int].connect(self._SH_AudioAlertDeviceButtonActivated)
......@@ -901,9 +902,15 @@ class PreferencesWindow(base_class, ui_class, metaclass=QSingleton):
self.prefix_button.addItem(item_text)
self.prefix_button.setCurrentIndex(self.prefix_button.findText(item_text))
self._update_pstn_example_label()
# Messages tab
self.message_replication_button.show()
self.message_replication_button.setChecked(account.sms.enable_message_replication)
else:
self.account_auto_answer.setText('Auto answer from all neighbours')
self.message_replication_button.hide()
def update_chat_preview(self):
blink_settings = BlinkSettings()
......@@ -1455,6 +1462,11 @@ class PreferencesWindow(base_class, ui_class, metaclass=QSingleton):
account.sms.enable_pgp = checked
account.save()
def _SH_MessageReplicationButtonClicked(self, checked):
account = self.selected_account
account.sms.enable_message_replication = checked
account.save()
# Audio devices signal handlers
def _SH_AudioAlertDeviceButtonActivated(self, index):
device = self.audio_alert_device_button.itemData(index)
......
......@@ -1279,43 +1279,21 @@
</size>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="11" column="1">
<widget class="QCheckBox" name="message_add_unknown_contacts_button">
<item row="3" column="0" colspan="2">
<widget class="QCheckBox" name="message_iscomposing_enabled_button">
<property name="text">
<string>Add unknown contacts to 'Messages' group in your contacts</string>
<string>Enable Is-Composing</string>
</property>
</widget>
</item>
<item row="10" column="1">
<widget class="QLabel" name="label_4">
<property name="enabled">
<bool>false</bool>
</property>
<item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="message_cpim_enabled_button">
<property name="text">
<string>If you turn off IMDN you won't be able to see receipts from other people</string>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="Line" name="line_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<string>Use CPIM envelope</string>
</property>
</widget>
</item>
<item row="1" column="1">
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="message_label">
<property name="font">
<font>
......@@ -1331,28 +1309,55 @@
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="QCheckBox" name="message_imdn_enabled_button">
<item row="12" column="0" colspan="2">
<widget class="QCheckBox" name="message_replication_button">
<property name="text">
<string>Enable IMDN (Read receipts)</string>
<string>Handle server replicated messages</string>
</property>
</widget>
</item>
<item row="2" column="1">
<item row="8" column="0" colspan="2">
<widget class="QCheckBox" name="message_add_unknown_contacts_button">
<property name="text">
<string>Add unknown contacts to 'Messages' group in your contacts</string>
</property>
</widget>
</item>
<item row="10" column="0" colspan="2">
<widget class="QLabel" name="history_label">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>History and synchronization</string>
</property>
</widget>
</item>
<item row="9" column="0" colspan="2">
<widget class="QCheckBox" name="message_pgp_enabled_button">
<property name="text">
<string>Enable message encryption with OpenPGP (if supported by receiver)</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="Line" name="line_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QCheckBox" name="message_iscomposing_enabled_button">
<item row="6" column="0" colspan="2">
<widget class="QCheckBox" name="message_imdn_enabled_button">
<property name="text">
<string>Enable Is-Composing</string>
<string>Enable IMDN (Read receipts)</string>
</property>
</widget>
</item>
<item row="7" column="1">
<item row="4" column="0" colspan="2">
<widget class="QLabel" name="label">
<property name="font">
<font>
......@@ -1365,23 +1370,46 @@
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QCheckBox" name="message_cpim_enabled_button">
<item row="7" column="0" colspan="2">
<widget class="QLabel" name="label_4">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Use CPIM envelope</string>
<string>If you turn off IMDN you won't be able to see receipts from other people</string>
</property>
</widget>
</item>
<item row="12" column="1">
<widget class="QCheckBox" name="message_pgp_enabled_button">
<property name="text">
<string>Enable message encryption with OpenPGP (if supported by receiver)</string>
<item row="5" column="0" colspan="2">
<widget class="Line" name="line_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="11" column="0" colspan="2">
<widget class="Line" name="history_line">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="1" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="advanced_tab">
......@@ -2973,14 +3001,14 @@
<string>File Logging Settings</string>
</property>
<layout class="QGridLayout" name="logging_group_box_layout">
<item row="0" column="0" colspan="5">
<item row="0" column="0">
<widget class="QCheckBox" name="trace_sip_button">
<property name="text">
<string>Trace SIP</string>
</property>
</widget>
</item>
<item row="1" column="0">
<item row="1" column="0" colspan="5">
<widget class="QCheckBox" name="trace_messaging_button">
<property name="text">
<string>Trace SIP Messaging and PGP Encryption</string>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment