9578053 Jan 22 2022 distfiles.gentoo.org/distfiles/gajim-1.3.3-2.tar.gz
This commit is contained in:
parent
a5b3822651
commit
4c1b226bff
1045 changed files with 753037 additions and 18 deletions
404
gajim/common/modules/httpupload.py
Normal file
404
gajim/common/modules/httpupload.py
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
# This file is part of Gajim.
|
||||
#
|
||||
# Gajim is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation; version 3 only.
|
||||
#
|
||||
# Gajim is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# XEP-0363: HTTP File Upload
|
||||
|
||||
|
||||
import os
|
||||
import io
|
||||
from urllib.parse import urlparse
|
||||
import mimetypes
|
||||
|
||||
from nbxmpp.namespaces import Namespace
|
||||
from nbxmpp.errors import StanzaError
|
||||
from nbxmpp.errors import MalformedStanzaError
|
||||
from nbxmpp.errors import HTTPUploadStanzaError
|
||||
from nbxmpp.util import convert_tls_error_flags
|
||||
from gi.repository import GLib
|
||||
from gi.repository import Soup
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.i18n import _
|
||||
from gajim.common.helpers import get_tls_error_phrase
|
||||
from gajim.common.helpers import get_user_proxy
|
||||
from gajim.common.const import FTState
|
||||
from gajim.common.filetransfer import FileTransfer
|
||||
from gajim.common.modules.base import BaseModule
|
||||
from gajim.common.exceptions import FileError
|
||||
|
||||
|
||||
class HTTPUpload(BaseModule):
|
||||
|
||||
_nbxmpp_extends = 'HTTPUpload'
|
||||
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
self.available = False
|
||||
self.component = None
|
||||
self.httpupload_namespace = None
|
||||
self.max_file_size = None # maximum file size in bytes
|
||||
|
||||
self._proxy_resolver = None
|
||||
self._queued_messages = {}
|
||||
self._session = Soup.Session()
|
||||
self._session.props.ssl_strict = False
|
||||
self._session.props.user_agent = 'Gajim %s' % app.version
|
||||
|
||||
def _set_proxy_if_available(self):
|
||||
proxy = get_user_proxy(self._account)
|
||||
if proxy is None:
|
||||
self._proxy_resolver = None
|
||||
self._session.props.proxy_resolver = None
|
||||
else:
|
||||
self._proxy_resolver = proxy.get_resolver()
|
||||
self._session.props.proxy_resolver = self._proxy_resolver
|
||||
|
||||
def pass_disco(self, info):
|
||||
if not info.has_httpupload:
|
||||
return
|
||||
|
||||
self.available = True
|
||||
self.httpupload_namespace = Namespace.HTTPUPLOAD_0
|
||||
self.component = info.jid
|
||||
self.max_file_size = info.httpupload_max_file_size
|
||||
|
||||
self._log.info('Discovered component: %s', info.jid)
|
||||
|
||||
if self.max_file_size is None:
|
||||
self._log.warning('Component does not provide maximum file size')
|
||||
else:
|
||||
size = GLib.format_size_full(self.max_file_size,
|
||||
GLib.FormatSizeFlags.IEC_UNITS)
|
||||
self._log.info('Component has a maximum file size of: %s', size)
|
||||
|
||||
for ctrl in app.interface.msg_win_mgr.get_controls(acct=self._account):
|
||||
ctrl.update_actions()
|
||||
|
||||
def make_transfer(self, path, encryption, contact, groupchat=False):
|
||||
if not path or not os.path.exists(path):
|
||||
raise FileError(_('Could not access file'))
|
||||
|
||||
invalid_file = False
|
||||
stat = os.stat(path)
|
||||
|
||||
if os.path.isfile(path):
|
||||
if stat[6] == 0:
|
||||
invalid_file = True
|
||||
msg = _('File is empty')
|
||||
else:
|
||||
invalid_file = True
|
||||
msg = _('File does not exist')
|
||||
|
||||
if self.max_file_size is not None and \
|
||||
stat.st_size > self.max_file_size:
|
||||
invalid_file = True
|
||||
size = GLib.format_size_full(self.max_file_size,
|
||||
GLib.FormatSizeFlags.IEC_UNITS)
|
||||
msg = _('File is too large, '
|
||||
'maximum allowed file size is: %s') % size
|
||||
|
||||
if invalid_file:
|
||||
raise FileError(msg)
|
||||
|
||||
mime = mimetypes.MimeTypes().guess_type(path)[0]
|
||||
if not mime:
|
||||
mime = 'application/octet-stream' # fallback mime type
|
||||
self._log.info("Detected MIME type of file: %s", mime)
|
||||
|
||||
return HTTPFileTransfer(self._account,
|
||||
path,
|
||||
contact,
|
||||
mime,
|
||||
encryption,
|
||||
groupchat)
|
||||
|
||||
def cancel_transfer(self, transfer):
|
||||
transfer.set_cancelled()
|
||||
message = self._queued_messages.get(id(transfer))
|
||||
if message is None:
|
||||
return
|
||||
|
||||
self._session.cancel_message(message, Soup.Status.CANCELLED)
|
||||
|
||||
def start_transfer(self, transfer):
|
||||
if transfer.encryption is not None and not transfer.is_encrypted:
|
||||
transfer.set_encrypting()
|
||||
plugin = app.plugin_manager.encryption_plugins[transfer.encryption]
|
||||
if hasattr(plugin, 'encrypt_file'):
|
||||
plugin.encrypt_file(transfer,
|
||||
self._account,
|
||||
self.start_transfer)
|
||||
else:
|
||||
transfer.set_error('encryption-not-available')
|
||||
|
||||
return
|
||||
|
||||
transfer.set_preparing()
|
||||
self._log.info('Sending request for slot')
|
||||
self._nbxmpp('HTTPUpload').request_slot(
|
||||
jid=self.component,
|
||||
filename=transfer.filename,
|
||||
size=transfer.size,
|
||||
content_type=transfer.mime,
|
||||
callback=self._received_slot,
|
||||
user_data=transfer)
|
||||
|
||||
def _received_slot(self, task):
|
||||
transfer = task.get_user_data()
|
||||
|
||||
try:
|
||||
result = task.finish()
|
||||
except (StanzaError,
|
||||
HTTPUploadStanzaError,
|
||||
MalformedStanzaError) as error:
|
||||
|
||||
if error.app_condition == 'file-too-large':
|
||||
size_text = GLib.format_size_full(
|
||||
error.get_max_file_size(),
|
||||
GLib.FormatSizeFlags.IEC_UNITS)
|
||||
|
||||
error_text = _('File is too large, '
|
||||
'maximum allowed file size is: %s' % size_text)
|
||||
transfer.set_error('file-too-large', error_text)
|
||||
|
||||
else:
|
||||
transfer.set_error('misc', str(error))
|
||||
|
||||
return
|
||||
|
||||
transfer.process_result(result)
|
||||
|
||||
if (urlparse(transfer.put_uri).scheme != 'https' or
|
||||
urlparse(transfer.get_uri).scheme != 'https'):
|
||||
transfer.set_error('unsecure')
|
||||
return
|
||||
|
||||
self._log.info('Uploading file to %s', transfer.put_uri)
|
||||
self._log.info('Please download from %s', transfer.get_uri)
|
||||
|
||||
self._upload_file(transfer)
|
||||
|
||||
def _upload_file(self, transfer):
|
||||
transfer.set_started()
|
||||
|
||||
message = Soup.Message.new('PUT', transfer.put_uri)
|
||||
message.connect('starting', self._check_certificate, transfer)
|
||||
|
||||
# Set CAN_REBUILD so chunks get discarded after they have been
|
||||
# written to the network
|
||||
message.set_flags(Soup.MessageFlags.CAN_REBUILD |
|
||||
Soup.MessageFlags.NO_REDIRECT)
|
||||
|
||||
message.props.request_body.set_accumulate(False)
|
||||
|
||||
message.props.request_headers.set_content_type(transfer.mime, None)
|
||||
message.props.request_headers.set_content_length(transfer.size)
|
||||
for name, value in transfer.headers.items():
|
||||
message.props.request_headers.append(name, value)
|
||||
|
||||
message.connect('wrote-headers', self._on_wrote_headers, transfer)
|
||||
message.connect('wrote-chunk', self._on_wrote_chunk, transfer)
|
||||
|
||||
self._queued_messages[id(transfer)] = message
|
||||
self._set_proxy_if_available()
|
||||
self._session.queue_message(message, self._on_finish, transfer)
|
||||
|
||||
def _check_certificate(self, message, transfer):
|
||||
https_used, tls_certificate, tls_errors = message.get_https_status()
|
||||
if not https_used:
|
||||
self._log.warning('HTTPS was not used for upload')
|
||||
transfer.set_error('unsecure')
|
||||
self._session.cancel_message(message, Soup.Status.CANCELLED)
|
||||
return
|
||||
|
||||
tls_errors = convert_tls_error_flags(tls_errors)
|
||||
if app.cert_store.verify(tls_certificate, tls_errors):
|
||||
return
|
||||
|
||||
for error in tls_errors:
|
||||
phrase = get_tls_error_phrase(error)
|
||||
self._log.warning('TLS verification failed: %s', phrase)
|
||||
|
||||
transfer.set_error('tls-verification-failed', phrase)
|
||||
self._session.cancel_message(message, Soup.Status.CANCELLED)
|
||||
|
||||
def _on_finish(self, _session, message, transfer):
|
||||
self._queued_messages.pop(id(transfer), None)
|
||||
|
||||
if message.props.status_code == Soup.Status.CANCELLED:
|
||||
self._log.info('Upload cancelled')
|
||||
return
|
||||
|
||||
if message.props.status_code in (Soup.Status.OK, Soup.Status.CREATED):
|
||||
self._log.info('Upload completed successfully')
|
||||
transfer.set_finished()
|
||||
|
||||
|
||||
else:
|
||||
phrase = Soup.Status.get_phrase(message.props.status_code)
|
||||
self._log.error('Got unexpected http upload response code: %s',
|
||||
phrase)
|
||||
transfer.set_error('http-response', phrase)
|
||||
|
||||
def _on_wrote_chunk(self, message, transfer):
|
||||
transfer.update_progress()
|
||||
if transfer.is_complete:
|
||||
message.props.request_body.complete()
|
||||
return
|
||||
|
||||
bytes_ = transfer.get_chunk()
|
||||
self._session.pause_message(message)
|
||||
GLib.idle_add(self._append, message, bytes_)
|
||||
|
||||
def _append(self, message, bytes_):
|
||||
if message.props.status_code == Soup.Status.CANCELLED:
|
||||
return
|
||||
self._session.unpause_message(message)
|
||||
message.props.request_body.append(bytes_)
|
||||
|
||||
@staticmethod
|
||||
def _on_wrote_headers(message, transfer):
|
||||
message.props.request_body.append(transfer.get_chunk())
|
||||
|
||||
|
||||
class HTTPFileTransfer(FileTransfer):
|
||||
|
||||
_state_descriptions = {
|
||||
FTState.ENCRYPTING: _('Encrypting file…'),
|
||||
FTState.PREPARING: _('Requesting HTTP File Upload Slot…'),
|
||||
FTState.STARTED: _('Uploading via HTTP File Upload…'),
|
||||
}
|
||||
|
||||
_errors = {
|
||||
'unsecure': _('The server returned an insecure transport (HTTP).'),
|
||||
'encryption-not-available': _('There is no encryption method available '
|
||||
'for the chosen encryption.')
|
||||
}
|
||||
|
||||
def __init__(self,
|
||||
account,
|
||||
path,
|
||||
contact,
|
||||
mime,
|
||||
encryption,
|
||||
groupchat):
|
||||
|
||||
FileTransfer.__init__(self, account)
|
||||
|
||||
self._path = path
|
||||
self._encryption = encryption
|
||||
self._groupchat = groupchat
|
||||
self._contact = contact
|
||||
self._mime = mime
|
||||
|
||||
self.size = os.stat(path).st_size
|
||||
self.put_uri = None
|
||||
self.get_uri = None
|
||||
self._uri_transform_func = None
|
||||
|
||||
self._stream = None
|
||||
self._data = None
|
||||
self._headers = {}
|
||||
|
||||
self._is_encrypted = False
|
||||
|
||||
@property
|
||||
def mime(self):
|
||||
return self._mime
|
||||
|
||||
@property
|
||||
def contact(self):
|
||||
return self._contact
|
||||
|
||||
@property
|
||||
def is_groupchat(self):
|
||||
return self._groupchat
|
||||
|
||||
@property
|
||||
def encryption(self):
|
||||
return self._encryption
|
||||
|
||||
@property
|
||||
def headers(self):
|
||||
return self._headers
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return self._path
|
||||
|
||||
@property
|
||||
def is_encrypted(self):
|
||||
return self._is_encrypted
|
||||
|
||||
def get_transformed_uri(self):
|
||||
if self._uri_transform_func is not None:
|
||||
return self._uri_transform_func(self.get_uri)
|
||||
return self.get_uri
|
||||
|
||||
def set_uri_transform_func(self, func):
|
||||
self._uri_transform_func = func
|
||||
|
||||
@property
|
||||
def filename(self):
|
||||
return os.path.basename(self._path)
|
||||
|
||||
def set_error(self, domain, text=''):
|
||||
if not text:
|
||||
text = self._errors[domain]
|
||||
|
||||
self._close()
|
||||
super().set_error(domain, text)
|
||||
|
||||
def set_finished(self):
|
||||
self._close()
|
||||
super().set_finished()
|
||||
|
||||
def set_encrypted_data(self, data):
|
||||
self._data = data
|
||||
self._is_encrypted = True
|
||||
|
||||
def _close(self):
|
||||
if self._stream is not None:
|
||||
self._stream.close()
|
||||
|
||||
def get_chunk(self):
|
||||
if self._stream is None:
|
||||
if self._encryption is None:
|
||||
self._stream = open(self._path, 'rb')
|
||||
else:
|
||||
self._stream = io.BytesIO(self._data)
|
||||
|
||||
data = self._stream.read(16384)
|
||||
if not data:
|
||||
self._close()
|
||||
return None
|
||||
self._seen += len(data)
|
||||
if self.is_complete:
|
||||
self._close()
|
||||
return data
|
||||
|
||||
def get_data(self):
|
||||
with open(self._path, 'rb') as file:
|
||||
data = file.read()
|
||||
return data
|
||||
|
||||
def process_result(self, result):
|
||||
self.put_uri = result.put_uri
|
||||
self.get_uri = result.get_uri
|
||||
self._headers = result.headers
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return HTTPUpload(*args, **kwargs), 'HTTPUpload'
|
||||
Loading…
Add table
Add a link
Reference in a new issue