Name of filetransfer content is now random to be able to have 2 transfer in the same session. send and handle content-add in filetranfer
This commit is contained in:
parent
f03cdbbebf
commit
286d788da0
|
@ -48,7 +48,7 @@ class ConnectionJingle(object):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# dictionary: sessionid => JingleSession object
|
# dictionary: sessionid => JingleSession object
|
||||||
self.__sessions = {}
|
self.__sessions__ = {}
|
||||||
|
|
||||||
# dictionary: (jid, iq stanza id) => JingleSession object,
|
# dictionary: (jid, iq stanza id) => JingleSession object,
|
||||||
# one time callbacks
|
# one time callbacks
|
||||||
|
@ -58,12 +58,12 @@ class ConnectionJingle(object):
|
||||||
"""
|
"""
|
||||||
Remove a jingle session from a jingle stanza dispatcher
|
Remove a jingle session from a jingle stanza dispatcher
|
||||||
"""
|
"""
|
||||||
if sid in self.__sessions:
|
if sid in self.__sessions__:
|
||||||
#FIXME: Move this elsewhere?
|
#FIXME: Move this elsewhere?
|
||||||
for content in self.__sessions[sid].contents.values():
|
for content in self.__sessions__[sid].contents.values():
|
||||||
content.destroy()
|
content.destroy()
|
||||||
self.__sessions[sid].callbacks = []
|
self.__sessions__[sid].callbacks = []
|
||||||
del self.__sessions[sid]
|
del self.__sessions__[sid]
|
||||||
|
|
||||||
def _JingleCB(self, con, stanza):
|
def _JingleCB(self, con, stanza):
|
||||||
"""
|
"""
|
||||||
|
@ -91,23 +91,23 @@ class ConnectionJingle(object):
|
||||||
sid = jingle.getAttr('sid')
|
sid = jingle.getAttr('sid')
|
||||||
else:
|
else:
|
||||||
sid = None
|
sid = None
|
||||||
for sesn in self.__sessions.values():
|
for sesn in self.__sessions__.values():
|
||||||
if id in sesn.iq_ids:
|
if id in sesn.iq_ids:
|
||||||
sesn.on_stanza(stanza)
|
sesn.on_stanza(stanza)
|
||||||
return
|
return
|
||||||
|
|
||||||
# do we need to create a new jingle object
|
# do we need to create a new jingle object
|
||||||
if sid not in self.__sessions:
|
if sid not in self.__sessions__:
|
||||||
#TODO: tie-breaking and other things...
|
#TODO: tie-breaking and other things...
|
||||||
newjingle = JingleSession(con=self, weinitiate=False, jid=jid,
|
newjingle = JingleSession(con=self, weinitiate=False, jid=jid,
|
||||||
iq_id = id, sid=sid)
|
iq_id = id, sid=sid)
|
||||||
self.__sessions[sid] = newjingle
|
self.__sessions__[sid] = newjingle
|
||||||
|
|
||||||
# we already have such session in dispatcher...
|
# we already have such session in dispatcher...
|
||||||
self.__sessions[sid].collect_iq_id(id)
|
self.__sessions__[sid].collect_iq_id(id)
|
||||||
self.__sessions[sid].on_stanza(stanza)
|
self.__sessions__[sid].on_stanza(stanza)
|
||||||
# Delete invalid/unneeded sessions
|
# Delete invalid/unneeded sessions
|
||||||
if sid in self.__sessions and self.__sessions[sid].state == JingleStates.ended:
|
if sid in self.__sessions__ and self.__sessions__[sid].state == JingleStates.ended:
|
||||||
self.delete_jingle_session(sid)
|
self.delete_jingle_session(sid)
|
||||||
|
|
||||||
raise xmpp.NodeProcessed
|
raise xmpp.NodeProcessed
|
||||||
|
@ -120,7 +120,7 @@ class ConnectionJingle(object):
|
||||||
jingle.add_content('voice', JingleAudio(jingle))
|
jingle.add_content('voice', JingleAudio(jingle))
|
||||||
else:
|
else:
|
||||||
jingle = JingleSession(self, weinitiate=True, jid=jid)
|
jingle = JingleSession(self, weinitiate=True, jid=jid)
|
||||||
self.__sessions[jingle.sid] = jingle
|
self.__sessions__[jingle.sid] = jingle
|
||||||
jingle.add_content('voice', JingleAudio(jingle))
|
jingle.add_content('voice', JingleAudio(jingle))
|
||||||
jingle.start_session()
|
jingle.start_session()
|
||||||
return jingle.sid
|
return jingle.sid
|
||||||
|
@ -133,7 +133,7 @@ class ConnectionJingle(object):
|
||||||
jingle.add_content('video', JingleVideo(jingle))
|
jingle.add_content('video', JingleVideo(jingle))
|
||||||
else:
|
else:
|
||||||
jingle = JingleSession(self, weinitiate=True, jid=jid)
|
jingle = JingleSession(self, weinitiate=True, jid=jid)
|
||||||
self.__sessions[jingle.sid] = jingle
|
self.__sessions__[jingle.sid] = jingle
|
||||||
jingle.add_content('video', JingleVideo(jingle))
|
jingle.add_content('video', JingleVideo(jingle))
|
||||||
jingle.start_session()
|
jingle.start_session()
|
||||||
return jingle.sid
|
return jingle.sid
|
||||||
|
@ -150,24 +150,25 @@ class ConnectionJingle(object):
|
||||||
file_props['sid'] = jingle.sid
|
file_props['sid'] = jingle.sid
|
||||||
c = JingleFileTransfer(jingle, file_props=file_props,
|
c = JingleFileTransfer(jingle, file_props=file_props,
|
||||||
use_security=use_security)
|
use_security=use_security)
|
||||||
jingle.add_content('file', c)
|
jingle.add_content('file' + helpers.get_random_string_16(), c)
|
||||||
|
jingle.on_session_state_changed(c)
|
||||||
else:
|
else:
|
||||||
jingle = JingleSession(self, weinitiate=True, jid=jid)
|
jingle = JingleSession(self, weinitiate=True, jid=jid)
|
||||||
self.__sessions[jingle.sid] = jingle
|
self.__sessions__[jingle.sid] = jingle
|
||||||
file_props['sid'] = jingle.sid
|
file_props['sid'] = jingle.sid
|
||||||
c = JingleFileTransfer(jingle, file_props=file_props,
|
c = JingleFileTransfer(jingle, file_props=file_props,
|
||||||
use_security=use_security)
|
use_security=use_security)
|
||||||
jingle.add_content('file', c)
|
jingle.add_content('file' + helpers.get_random_string_16(), c)
|
||||||
jingle.start_session()
|
jingle.start_session()
|
||||||
return c.transport.sid
|
return c.transport.sid
|
||||||
|
|
||||||
|
|
||||||
def iter_jingle_sessions(self, jid, sid=None, media=None):
|
def iter_jingle_sessions(self, jid, sid=None, media=None):
|
||||||
if sid:
|
if sid:
|
||||||
return (session for session in self.__sessions.values() if session.sid == sid)
|
return (session for session in self.__sessions__.values() if session.sid == sid)
|
||||||
sessions = (session for session in self.__sessions.values() if session.peerjid == jid)
|
sessions = (session for session in self.__sessions__.values() if session.peerjid == jid)
|
||||||
if media:
|
if media:
|
||||||
if media not in ('audio', 'video'):
|
if media not in ('audio', 'video', 'file'):
|
||||||
return tuple()
|
return tuple()
|
||||||
else:
|
else:
|
||||||
return (session for session in sessions if session.get_content(media))
|
return (session for session in sessions if session.get_content(media))
|
||||||
|
@ -177,14 +178,14 @@ class ConnectionJingle(object):
|
||||||
|
|
||||||
def get_jingle_session(self, jid, sid=None, media=None):
|
def get_jingle_session(self, jid, sid=None, media=None):
|
||||||
if sid:
|
if sid:
|
||||||
if sid in self.__sessions:
|
if sid in self.__sessions__:
|
||||||
return self.__sessions[sid]
|
return self.__sessions__[sid]
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
elif media:
|
elif media:
|
||||||
if media not in ('audio', 'video'):
|
if media not in ('audio', 'video', 'file'):
|
||||||
return None
|
return None
|
||||||
for session in self.__sessions.values():
|
for session in self.__sessions__.values():
|
||||||
if session.peerjid == jid and session.get_content(media):
|
if session.peerjid == jid and session.get_content(media):
|
||||||
return session
|
return session
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,7 @@ class JingleFileTransfer(JingleContent):
|
||||||
|
|
||||||
# events we might be interested in
|
# events we might be interested in
|
||||||
self.callbacks['session-initiate'] += [self.__on_session_initiate]
|
self.callbacks['session-initiate'] += [self.__on_session_initiate]
|
||||||
|
self.callbacks['content-add'] += [self.__on_session_initiate]
|
||||||
self.callbacks['session-accept'] += [self.__on_session_accept]
|
self.callbacks['session-accept'] += [self.__on_session_accept]
|
||||||
self.callbacks['session-terminate'] += [self.__on_session_terminate]
|
self.callbacks['session-terminate'] += [self.__on_session_terminate]
|
||||||
self.callbacks['transport-accept'] += [self.__on_transport_accept]
|
self.callbacks['transport-accept'] += [self.__on_transport_accept]
|
||||||
|
@ -249,7 +250,7 @@ class JingleFileTransfer(JingleContent):
|
||||||
|
|
||||||
content = xmpp.Node('content')
|
content = xmpp.Node('content')
|
||||||
content.setAttr('creator', 'initiator')
|
content.setAttr('creator', 'initiator')
|
||||||
content.setAttr('name', 'file')
|
content.setAttr('name', self.name)
|
||||||
|
|
||||||
transport = xmpp.Node('transport')
|
transport = xmpp.Node('transport')
|
||||||
transport.setNamespace(xmpp.NS_JINGLE_BYTESTREAM)
|
transport.setNamespace(xmpp.NS_JINGLE_BYTESTREAM)
|
||||||
|
|
|
@ -85,14 +85,14 @@ class JingleSession(object):
|
||||||
if not sid:
|
if not sid:
|
||||||
sid = con.connection.getAnID()
|
sid = con.connection.getAnID()
|
||||||
self.sid = sid # sessionid
|
self.sid = sid # sessionid
|
||||||
|
|
||||||
# iq stanza id, used to determine which sessions to summon callback
|
# iq stanza id, used to determine which sessions to summon callback
|
||||||
# later on when iq-result stanza arrives
|
# later on when iq-result stanza arrives
|
||||||
if iq_id is not None:
|
if iq_id is not None:
|
||||||
self.iq_ids = [iq_id]
|
self.iq_ids = [iq_id]
|
||||||
else:
|
else:
|
||||||
self.iq_ids = []
|
self.iq_ids = []
|
||||||
|
|
||||||
|
|
||||||
self.accepted = True # is this session accepted by user
|
self.accepted = True # is this session accepted by user
|
||||||
|
|
||||||
|
@ -127,7 +127,7 @@ class JingleSession(object):
|
||||||
def collect_iq_id(self, iq_id):
|
def collect_iq_id(self, iq_id):
|
||||||
if iq_id is not None:
|
if iq_id is not None:
|
||||||
self.iq_ids.append(iq_id)
|
self.iq_ids.append(iq_id)
|
||||||
|
|
||||||
def approve_session(self):
|
def approve_session(self):
|
||||||
"""
|
"""
|
||||||
Called when user accepts session in UI (when we aren't the initiator)
|
Called when user accepts session in UI (when we aren't the initiator)
|
||||||
|
@ -340,7 +340,7 @@ class JingleSession(object):
|
||||||
break
|
break
|
||||||
elif child.getNamespace() == xmpp.NS_STANZAS:
|
elif child.getNamespace() == xmpp.NS_STANZAS:
|
||||||
error_name = child.getName()
|
error_name = child.getName()
|
||||||
self.__dispatch_error(error_name, text, error.getAttribute('type'))
|
self.__dispatch_error(error_name, text, error.getAttr('type'))
|
||||||
# FIXME: Not sure when we would want to do that...
|
# FIXME: Not sure when we would want to do that...
|
||||||
|
|
||||||
def __on_transport_replace(self, stanza, jingle, error, action):
|
def __on_transport_replace(self, stanza, jingle, error, action):
|
||||||
|
@ -482,13 +482,13 @@ class JingleSession(object):
|
||||||
# for cn in self.contents.values():
|
# for cn in self.contents.values():
|
||||||
# cn.on_stanza(stanza, None, error, action)
|
# cn.on_stanza(stanza, None, error, action)
|
||||||
# return
|
# return
|
||||||
|
|
||||||
# special case: iq-result stanza does not come with a jingle element
|
# special case: iq-result stanza does not come with a jingle element
|
||||||
if action == 'iq-result':
|
if action == 'iq-result':
|
||||||
for cn in self.contents.values():
|
for cn in self.contents.values():
|
||||||
cn.on_stanza(stanza, None, error, action)
|
cn.on_stanza(stanza, None, error, action)
|
||||||
return
|
return
|
||||||
|
|
||||||
for content in jingle.iterTags('content'):
|
for content in jingle.iterTags('content'):
|
||||||
name = content['name']
|
name = content['name']
|
||||||
creator = content['creator']
|
creator = content['creator']
|
||||||
|
@ -673,7 +673,8 @@ class JingleSession(object):
|
||||||
stanza, jingle = self.__make_jingle('content-add')
|
stanza, jingle = self.__make_jingle('content-add')
|
||||||
self.__append_content(jingle, content)
|
self.__append_content(jingle, content)
|
||||||
self.__broadcast(stanza, jingle, None, 'content-add-sent')
|
self.__broadcast(stanza, jingle, None, 'content-add-sent')
|
||||||
self.connection.connection.send(stanza)
|
id_ = self.connection.connection.send(stanza)
|
||||||
|
self.collect_iq_id(id_)
|
||||||
|
|
||||||
def __content_accept(self, content):
|
def __content_accept(self, content):
|
||||||
# TODO: test
|
# TODO: test
|
||||||
|
@ -681,7 +682,8 @@ class JingleSession(object):
|
||||||
stanza, jingle = self.__make_jingle('content-accept')
|
stanza, jingle = self.__make_jingle('content-accept')
|
||||||
self.__append_content(jingle, content)
|
self.__append_content(jingle, content)
|
||||||
self.__broadcast(stanza, jingle, None, 'content-accept-sent')
|
self.__broadcast(stanza, jingle, None, 'content-accept-sent')
|
||||||
self.connection.connection.send(stanza)
|
id_ = self.connection.connection.send(stanza)
|
||||||
|
self.collect_iq_id(id_)
|
||||||
|
|
||||||
def __content_reject(self, content):
|
def __content_reject(self, content):
|
||||||
assert self.state != JingleStates.ended
|
assert self.state != JingleStates.ended
|
||||||
|
|
|
@ -224,6 +224,13 @@ class JingleTransportSocks5(JingleTransport):
|
||||||
proxy_cand.append(c)
|
proxy_cand.append(c)
|
||||||
self.candidates += proxy_cand
|
self.candidates += proxy_cand
|
||||||
|
|
||||||
|
def get_content(self):
|
||||||
|
sesn = self.connection.get_jingle_session(self.ourjid,
|
||||||
|
self.file_props['session-sid'])
|
||||||
|
for content in sesn.contents.values():
|
||||||
|
if content.transport == self:
|
||||||
|
return content
|
||||||
|
|
||||||
def _on_proxy_auth_ok(self, proxy):
|
def _on_proxy_auth_ok(self, proxy):
|
||||||
log.info('proxy auth ok for ' + str(proxy))
|
log.info('proxy auth ok for ' + str(proxy))
|
||||||
# send activate request to proxy, send activated confirmation to peer
|
# send activate request to proxy, send activated confirmation to peer
|
||||||
|
@ -242,15 +249,15 @@ class JingleTransportSocks5(JingleTransport):
|
||||||
|
|
||||||
content = xmpp.Node('content')
|
content = xmpp.Node('content')
|
||||||
content.setAttr('creator', 'initiator')
|
content.setAttr('creator', 'initiator')
|
||||||
content.setAttr('name', 'file')
|
c = self.get_content()
|
||||||
|
content.setAttr('name', c.name)
|
||||||
transport = xmpp.Node('transport')
|
transport = xmpp.Node('transport')
|
||||||
transport.setNamespace(xmpp.NS_JINGLE_BYTESTREAM)
|
transport.setNamespace(xmpp.NS_JINGLE_BYTESTREAM)
|
||||||
activated = xmpp.Node('activated')
|
activated = xmpp.Node('activated')
|
||||||
cid = None
|
cid = None
|
||||||
for host in self.candidates:
|
for host in self.candidates:
|
||||||
if host['host'] == proxy['host'] and \
|
if host['host'] == proxy['host'] and host['jid'] == proxy['jid'] \
|
||||||
host['jid'] == proxy['jid'] and \
|
and host['port'] == proxy['port']:
|
||||||
host['port'] == proxy['port']:
|
|
||||||
cid = host['candidate_id']
|
cid = host['candidate_id']
|
||||||
break
|
break
|
||||||
if cid is None:
|
if cid is None:
|
||||||
|
|
|
@ -29,7 +29,7 @@ pending_sessions = {} # key-exchange id -> session, accept that session once key
|
||||||
|
|
||||||
def key_exchange_pend(id, session):
|
def key_exchange_pend(id, session):
|
||||||
pending_sessions[id] = session
|
pending_sessions[id] = session
|
||||||
|
|
||||||
def approve_pending_session(id):
|
def approve_pending_session(id):
|
||||||
session = pending_sessions[id]
|
session = pending_sessions[id]
|
||||||
session.approve_session()
|
session.approve_session()
|
||||||
|
@ -45,9 +45,11 @@ if PYOPENSSL_PRESENT:
|
||||||
from OpenSSL import SSL
|
from OpenSSL import SSL
|
||||||
from OpenSSL.SSL import Context
|
from OpenSSL.SSL import Context
|
||||||
from OpenSSL import crypto
|
from OpenSSL import crypto
|
||||||
|
TYPE_RSA = crypto.TYPE_RSA
|
||||||
|
TYPE_DSA = crypto.TYPE_DSA
|
||||||
|
|
||||||
SELF_SIGNED_CERTIFICATE = 'localcert'
|
SELF_SIGNED_CERTIFICATE = 'localcert'
|
||||||
|
|
||||||
def default_callback(connection, certificate, error_num, depth, return_code):
|
def default_callback(connection, certificate, error_num, depth, return_code):
|
||||||
log.info("certificate: %s" % certificate)
|
log.info("certificate: %s" % certificate)
|
||||||
return return_code
|
return return_code
|
||||||
|
@ -95,7 +97,7 @@ def get_context(fingerprint, verify_cb=None):
|
||||||
ctx.set_verify(SSL.VERIFY_NONE|SSL.VERIFY_FAIL_IF_NO_PEER_CERT, verify_cb or default_callback)
|
ctx.set_verify(SSL.VERIFY_NONE|SSL.VERIFY_FAIL_IF_NO_PEER_CERT, verify_cb or default_callback)
|
||||||
elif fingerprint == 'client':
|
elif fingerprint == 'client':
|
||||||
ctx.set_verify(SSL.VERIFY_PEER, verify_cb or default_callback)
|
ctx.set_verify(SSL.VERIFY_PEER, verify_cb or default_callback)
|
||||||
|
|
||||||
cert_name = os.path.join(gajim.MY_CERT_DIR, SELF_SIGNED_CERTIFICATE)
|
cert_name = os.path.join(gajim.MY_CERT_DIR, SELF_SIGNED_CERTIFICATE)
|
||||||
ctx.use_privatekey_file (cert_name + '.pkey')
|
ctx.use_privatekey_file (cert_name + '.pkey')
|
||||||
ctx.use_certificate_file(cert_name + '.cert')
|
ctx.use_certificate_file(cert_name + '.cert')
|
||||||
|
@ -114,16 +116,16 @@ def send_cert(con, jid_from, sid):
|
||||||
certificate += line
|
certificate += line
|
||||||
iq = common.xmpp.Iq('result', to=jid_from);
|
iq = common.xmpp.Iq('result', to=jid_from);
|
||||||
iq.setAttr('id', sid)
|
iq.setAttr('id', sid)
|
||||||
|
|
||||||
pubkey = iq.setTag('pubkeys')
|
pubkey = iq.setTag('pubkeys')
|
||||||
pubkey.setNamespace(common.xmpp.NS_PUBKEY_PUBKEY)
|
pubkey.setNamespace(common.xmpp.NS_PUBKEY_PUBKEY)
|
||||||
|
|
||||||
keyinfo = pubkey.setTag('keyinfo')
|
keyinfo = pubkey.setTag('keyinfo')
|
||||||
name = keyinfo.setTag('name')
|
name = keyinfo.setTag('name')
|
||||||
name.setData('CertificateHash')
|
name.setData('CertificateHash')
|
||||||
cert = keyinfo.setTag('x509cert')
|
cert = keyinfo.setTag('x509cert')
|
||||||
cert.setData(certificate)
|
cert.setData(certificate)
|
||||||
|
|
||||||
con.send(iq)
|
con.send(iq)
|
||||||
|
|
||||||
def handle_new_cert(con, obj, jid_from):
|
def handle_new_cert(con, obj, jid_from):
|
||||||
|
@ -132,18 +134,18 @@ def handle_new_cert(con, obj, jid_from):
|
||||||
certpath += '.cert'
|
certpath += '.cert'
|
||||||
|
|
||||||
id = obj.getAttr('id')
|
id = obj.getAttr('id')
|
||||||
|
|
||||||
x509cert = obj.getTag('pubkeys').getTag('keyinfo').getTag('x509cert')
|
x509cert = obj.getTag('pubkeys').getTag('keyinfo').getTag('x509cert')
|
||||||
|
|
||||||
cert = x509cert.getData()
|
cert = x509cert.getData()
|
||||||
|
|
||||||
f = open(certpath, 'w')
|
f = open(certpath, 'w')
|
||||||
f.write('-----BEGIN CERTIFICATE-----\n')
|
f.write('-----BEGIN CERTIFICATE-----\n')
|
||||||
f.write(cert)
|
f.write(cert)
|
||||||
f.write('-----END CERTIFICATE-----\n')
|
f.write('-----END CERTIFICATE-----\n')
|
||||||
|
|
||||||
approve_pending_session(id)
|
approve_pending_session(id)
|
||||||
|
|
||||||
def send_cert_request(con, to_jid):
|
def send_cert_request(con, to_jid):
|
||||||
iq = common.xmpp.Iq('get', to=to_jid)
|
iq = common.xmpp.Iq('get', to=to_jid)
|
||||||
id = con.connection.getAnID()
|
id = con.connection.getAnID()
|
||||||
|
@ -155,9 +157,6 @@ def send_cert_request(con, to_jid):
|
||||||
|
|
||||||
# the following code is partly due to pyopenssl examples
|
# the following code is partly due to pyopenssl examples
|
||||||
|
|
||||||
TYPE_RSA = crypto.TYPE_RSA
|
|
||||||
TYPE_DSA = crypto.TYPE_DSA
|
|
||||||
|
|
||||||
def createKeyPair(type, bits):
|
def createKeyPair(type, bits):
|
||||||
"""
|
"""
|
||||||
Create a public/private key pair.
|
Create a public/private key pair.
|
||||||
|
|
|
@ -148,7 +148,8 @@ class ConnectionBytestream:
|
||||||
jingle_xtls.key_exchange_pend(id_, session)
|
jingle_xtls.key_exchange_pend(id_, session)
|
||||||
return
|
return
|
||||||
session.approve_session()
|
session.approve_session()
|
||||||
session.approve_content('file')
|
|
||||||
|
session.approve_content('file')
|
||||||
return
|
return
|
||||||
|
|
||||||
iq = xmpp.Iq(to=unicode(file_props['sender']), typ='result')
|
iq = xmpp.Iq(to=unicode(file_props['sender']), typ='result')
|
||||||
|
|
|
@ -914,7 +914,7 @@ class Socks5Listener(IdleObject):
|
||||||
# try the different possibilities (ipv6, ipv4, etc.)
|
# try the different possibilities (ipv6, ipv4, etc.)
|
||||||
try:
|
try:
|
||||||
self._serv = socket.socket(*ai[:3])
|
self._serv = socket.socket(*ai[:3])
|
||||||
if not self.fingerprint is None:
|
if self.fingerprint is not None:
|
||||||
self._serv = OpenSSL.SSL.Connection(
|
self._serv = OpenSSL.SSL.Connection(
|
||||||
jingle_xtls.get_context('server'), self._serv)
|
jingle_xtls.get_context('server'), self._serv)
|
||||||
except socket.error, e:
|
except socket.error, e:
|
||||||
|
|
Loading…
Reference in New Issue