pidgin/pidgin
Clone
Summary
Browse
Changes
Graph
fix heap-use-after-free in prefs removal
use-after-free
2019-11-10, Fabrice Bellet
0682910c702a
fix heap-use-after-free in prefs removal
/*
* purple - Jabber Protocol Plugin
*
* Purple is the legal property of its developers, whose names are too numerous
* to list here. Please refer to the COPYRIGHT file distributed with this
* source distribution.
*
* This program 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; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
*
*/
#include
"internal.h"
#include
"core.h"
#include
"debug.h"
#include
"prefs.h"
#include
"util.h"
#include
"buddy.h"
#include
"disco.h"
#include
"google/gmail.h"
#include
"google/google.h"
#include
"google/jingleinfo.h"
#include
"google/google_session.h"
#include
"iq.h"
#include
"jingle/jingle.h"
#include
"oob.h"
#include
"roster.h"
#include
"si.h"
#include
"ping.h"
#include
"adhoccommands.h"
#include
"data.h"
#include
"ibb.h"
static
GHashTable
*
iq_handlers
=
NULL
;
static
GHashTable
*
signal_iq_handlers
=
NULL
;
struct
_JabberIqCallbackData
{
JabberIqCallback
*
callback
;
gpointer
data
;
JabberID
*
to
;
};
void
jabber_iq_callbackdata_free
(
JabberIqCallbackData
*
jcd
)
{
jabber_id_free
(
jcd
->
to
);
g_free
(
jcd
);
}
JabberIq
*
jabber_iq_new
(
JabberStream
*
js
,
JabberIqType
type
)
{
JabberIq
*
iq
;
iq
=
g_new0
(
JabberIq
,
1
);
iq
->
type
=
type
;
iq
->
node
=
purple_xmlnode_new
(
"iq"
);
switch
(
iq
->
type
)
{
case
JABBER_IQ_SET
:
purple_xmlnode_set_attrib
(
iq
->
node
,
"type"
,
"set"
);
break
;
case
JABBER_IQ_GET
:
purple_xmlnode_set_attrib
(
iq
->
node
,
"type"
,
"get"
);
break
;
case
JABBER_IQ_ERROR
:
purple_xmlnode_set_attrib
(
iq
->
node
,
"type"
,
"error"
);
break
;
case
JABBER_IQ_RESULT
:
purple_xmlnode_set_attrib
(
iq
->
node
,
"type"
,
"result"
);
break
;
case
JABBER_IQ_NONE
:
/* this shouldn't ever happen */
break
;
}
iq
->
js
=
js
;
if
(
type
==
JABBER_IQ_GET
||
type
==
JABBER_IQ_SET
)
{
iq
->
id
=
jabber_get_next_id
(
js
);
purple_xmlnode_set_attrib
(
iq
->
node
,
"id"
,
iq
->
id
);
}
return
iq
;
}
JabberIq
*
jabber_iq_new_query
(
JabberStream
*
js
,
JabberIqType
type
,
const
char
*
xmlns
)
{
JabberIq
*
iq
=
jabber_iq_new
(
js
,
type
);
PurpleXmlNode
*
query
;
query
=
purple_xmlnode_new_child
(
iq
->
node
,
"query"
);
purple_xmlnode_set_namespace
(
query
,
xmlns
);
return
iq
;
}
void
jabber_iq_set_callback
(
JabberIq
*
iq
,
JabberIqCallback
*
callback
,
gpointer
data
)
{
iq
->
callback
=
callback
;
iq
->
callback_data
=
data
;
}
void
jabber_iq_set_id
(
JabberIq
*
iq
,
const
char
*
id
)
{
g_free
(
iq
->
id
);
if
(
id
)
{
purple_xmlnode_set_attrib
(
iq
->
node
,
"id"
,
id
);
iq
->
id
=
g_strdup
(
id
);
}
else
{
purple_xmlnode_remove_attrib
(
iq
->
node
,
"id"
);
iq
->
id
=
NULL
;
}
}
void
jabber_iq_send
(
JabberIq
*
iq
)
{
JabberIqCallbackData
*
jcd
;
g_return_if_fail
(
iq
!=
NULL
);
jabber_send
(
iq
->
js
,
iq
->
node
);
if
(
iq
->
id
&&
iq
->
callback
)
{
jcd
=
g_new0
(
JabberIqCallbackData
,
1
);
jcd
->
callback
=
iq
->
callback
;
jcd
->
data
=
iq
->
callback_data
;
jcd
->
to
=
jabber_id_new
(
purple_xmlnode_get_attrib
(
iq
->
node
,
"to"
));
g_hash_table_insert
(
iq
->
js
->
iq_callbacks
,
g_strdup
(
iq
->
id
),
jcd
);
}
jabber_iq_free
(
iq
);
}
void
jabber_iq_free
(
JabberIq
*
iq
)
{
g_return_if_fail
(
iq
!=
NULL
);
g_free
(
iq
->
id
);
purple_xmlnode_free
(
iq
->
node
);
g_free
(
iq
);
}
static
void
jabber_iq_last_parse
(
JabberStream
*
js
,
const
char
*
from
,
JabberIqType
type
,
const
char
*
id
,
PurpleXmlNode
*
packet
)
{
JabberIq
*
iq
;
PurpleXmlNode
*
query
;
char
*
idle_time
;
if
(
type
==
JABBER_IQ_GET
)
{
iq
=
jabber_iq_new_query
(
js
,
JABBER_IQ_RESULT
,
NS_LAST_ACTIVITY
);
jabber_iq_set_id
(
iq
,
id
);
if
(
from
)
purple_xmlnode_set_attrib
(
iq
->
node
,
"to"
,
from
);
query
=
purple_xmlnode_get_child
(
iq
->
node
,
"query"
);
idle_time
=
g_strdup_printf
(
"%ld"
,
js
->
idle
?
time
(
NULL
)
-
js
->
idle
:
0
);
purple_xmlnode_set_attrib
(
query
,
"seconds"
,
idle_time
);
g_free
(
idle_time
);
jabber_iq_send
(
iq
);
}
}
static
void
jabber_time_parse
(
JabberStream
*
js
,
const
char
*
from
,
JabberIqType
type
,
const
char
*
id
,
PurpleXmlNode
*
child
)
{
JabberIq
*
iq
;
if
(
type
==
JABBER_IQ_GET
)
{
PurpleXmlNode
*
tzo
,
*
utc
;
GDateTime
*
now
,
*
now_utc
;
gchar
*
date
,
*
tz
;
iq
=
jabber_iq_new
(
js
,
JABBER_IQ_RESULT
);
jabber_iq_set_id
(
iq
,
id
);
if
(
from
)
purple_xmlnode_set_attrib
(
iq
->
node
,
"to"
,
from
);
child
=
purple_xmlnode_new_child
(
iq
->
node
,
child
->
name
);
purple_xmlnode_set_namespace
(
child
,
NS_ENTITY_TIME
);
/* <tzo>-06:00</tzo> */
now
=
g_date_time_new_now_local
();
tz
=
g_date_time_format
(
now
,
"%:z"
);
tzo
=
purple_xmlnode_new_child
(
child
,
"tzo"
);
purple_xmlnode_insert_data
(
tzo
,
tz
,
-1
);
g_free
(
tz
);
/* <utc>2006-12-19T17:58:35Z</utc> */
now_utc
=
g_date_time_to_utc
(
now
);
date
=
g_date_time_format
(
now_utc
,
"%FT%TZ"
);
utc
=
purple_xmlnode_new_child
(
child
,
"utc"
);
purple_xmlnode_insert_data
(
utc
,
date
,
-1
);
g_free
(
date
);
g_date_time_unref
(
now
);
g_date_time_unref
(
now_utc
);
jabber_iq_send
(
iq
);
}
else
{
/* TODO: Errors */
}
}
static
void
jabber_iq_version_parse
(
JabberStream
*
js
,
const
char
*
from
,
JabberIqType
type
,
const
char
*
id
,
PurpleXmlNode
*
packet
)
{
JabberIq
*
iq
;
PurpleXmlNode
*
query
;
if
(
type
==
JABBER_IQ_GET
)
{
GHashTable
*
ui_info
;
const
char
*
ui_name
=
NULL
,
*
ui_version
=
NULL
;
iq
=
jabber_iq_new_query
(
js
,
JABBER_IQ_RESULT
,
"jabber:iq:version"
);
if
(
from
)
purple_xmlnode_set_attrib
(
iq
->
node
,
"to"
,
from
);
jabber_iq_set_id
(
iq
,
id
);
query
=
purple_xmlnode_get_child
(
iq
->
node
,
"query"
);
ui_info
=
purple_core_get_ui_info
();
if
(
NULL
!=
ui_info
)
{
ui_name
=
g_hash_table_lookup
(
ui_info
,
"name"
);
ui_version
=
g_hash_table_lookup
(
ui_info
,
"version"
);
}
if
(
NULL
!=
ui_name
&&
NULL
!=
ui_version
)
{
char
*
version_complete
=
g_strdup_printf
(
"%s (libpurple "
VERSION
")"
,
ui_version
);
purple_xmlnode_insert_data
(
purple_xmlnode_new_child
(
query
,
"name"
),
ui_name
,
-1
);
purple_xmlnode_insert_data
(
purple_xmlnode_new_child
(
query
,
"version"
),
version_complete
,
-1
);
g_free
(
version_complete
);
}
else
{
purple_xmlnode_insert_data
(
purple_xmlnode_new_child
(
query
,
"name"
),
"libpurple"
,
-1
);
purple_xmlnode_insert_data
(
purple_xmlnode_new_child
(
query
,
"version"
),
VERSION
,
-1
);
}
jabber_iq_send
(
iq
);
}
}
void
jabber_iq_remove_callback_by_id
(
JabberStream
*
js
,
const
char
*
id
)
{
g_hash_table_remove
(
js
->
iq_callbacks
,
id
);
}
/**
* Verify that the 'from' attribute of an IQ reply is a valid match for
* a given IQ request. The expected behavior is outlined in section
* 8.1.2.1 of the XMPP CORE spec (RFC 6120). We consider the reply to
* be a valid match if any of the following is true:
* - Request 'to' matches reply 'from' (including the case where
* neither are set).
* - Request 'to' was my JID (bare or full) and reply 'from' is empty.
* - Request 'to' was empty and reply 'from' is my JID. The spec says
* we should only allow bare JID, but we also allow full JID for
* compatibility with some servers.
* - Request 'to' was empty and reply 'from' is server JID. Not allowed by
* any spec, but for compatibility with some servers.
*
* These rules should allow valid IQ replies while preventing spoofed
* ones.
*
* For more discussion see the "Spoofing of iq ids and misbehaving
* servers" email thread from January 2014 on the jdev and security
* mailing lists. Also see https://developer.pidgin.im/ticket/15879
*
* @return TRUE if this reply is valid for the given request.
*/
static
gboolean
does_reply_from_match_request_to
(
JabberStream
*
js
,
JabberID
*
to
,
JabberID
*
from
)
{
if
(
jabber_id_equal
(
to
,
from
))
{
/* Request 'to' matches reply 'from' */
return
TRUE
;
}
if
(
!
from
&&
purple_strequal
(
to
->
node
,
js
->
user
->
node
)
&&
purple_strequal
(
to
->
domain
,
js
->
user
->
domain
))
{
/* Request 'to' was my JID (bare or full) and reply 'from' is empty */
return
TRUE
;
}
if
(
!
to
&&
purple_strequal
(
from
->
domain
,
js
->
user
->
domain
))
{
/* Request 'to' is empty and reply 'from' domain matches our domain */
if
(
!
from
->
node
&&
!
from
->
resource
)
{
/* Reply 'from' is server bare JID */
return
TRUE
;
}
if
(
purple_strequal
(
from
->
node
,
js
->
user
->
node
)
&&
(
!
from
->
resource
||
purple_strequal
(
from
->
resource
,
js
->
user
->
resource
)))
{
/* Reply 'from' is my full or bare JID */
return
TRUE
;
}
}
return
FALSE
;
}
void
jabber_iq_parse
(
JabberStream
*
js
,
PurpleXmlNode
*
packet
)
{
JabberIqCallbackData
*
jcd
;
PurpleXmlNode
*
child
,
*
error
,
*
x
;
const
char
*
xmlns
;
const
char
*
iq_type
,
*
id
,
*
from
;
JabberIqType
type
=
JABBER_IQ_NONE
;
gboolean
signal_return
;
JabberID
*
from_id
;
from
=
purple_xmlnode_get_attrib
(
packet
,
"from"
);
id
=
purple_xmlnode_get_attrib
(
packet
,
"id"
);
iq_type
=
purple_xmlnode_get_attrib
(
packet
,
"type"
);
/*
* Ensure the 'from' attribute is valid. No point in handling a stanza
* of which we don't understand where it came from.
*/
from_id
=
jabber_id_new
(
from
);
if
(
from
&&
!
from_id
)
{
purple_debug_error
(
"jabber"
,
"Received an iq with an invalid from: %s
\n
"
,
from
);
return
;
}
/*
* child will be either the first tag child or NULL if there is no child.
* Historically, we used just the 'query' subchild, but newer XEPs use
* differently named children. Grabbing the first child is (for the time
* being) sufficient.
*/
for
(
child
=
packet
->
child
;
child
;
child
=
child
->
next
)
{
if
(
child
->
type
==
PURPLE_XMLNODE_TYPE_TAG
)
break
;
}
if
(
iq_type
)
{
if
(
purple_strequal
(
iq_type
,
"get"
))
type
=
JABBER_IQ_GET
;
else
if
(
purple_strequal
(
iq_type
,
"set"
))
type
=
JABBER_IQ_SET
;
else
if
(
purple_strequal
(
iq_type
,
"result"
))
type
=
JABBER_IQ_RESULT
;
else
if
(
purple_strequal
(
iq_type
,
"error"
))
type
=
JABBER_IQ_ERROR
;
}
if
(
type
==
JABBER_IQ_NONE
)
{
purple_debug_error
(
"jabber"
,
"IQ with invalid type ('%s') - ignoring.
\n
"
,
iq_type
?
iq_type
:
"(null)"
);
jabber_id_free
(
from_id
);
return
;
}
/* All IQs must have an ID, so send an error for a set/get that doesn't */
if
(
!
id
||
!*
id
)
{
if
(
type
==
JABBER_IQ_SET
||
type
==
JABBER_IQ_GET
)
{
JabberIq
*
iq
=
jabber_iq_new
(
js
,
JABBER_IQ_ERROR
);
purple_xmlnode_free
(
iq
->
node
);
iq
->
node
=
purple_xmlnode_copy
(
packet
);
if
(
from
)
{
purple_xmlnode_set_attrib
(
iq
->
node
,
"to"
,
from
);
purple_xmlnode_remove_attrib
(
iq
->
node
,
"from"
);
}
purple_xmlnode_set_attrib
(
iq
->
node
,
"type"
,
"error"
);
/* This id is clearly not useful, but we must put something there for a valid stanza */
iq
->
id
=
jabber_get_next_id
(
js
);
purple_xmlnode_set_attrib
(
iq
->
node
,
"id"
,
iq
->
id
);
error
=
purple_xmlnode_new_child
(
iq
->
node
,
"error"
);
purple_xmlnode_set_attrib
(
error
,
"type"
,
"modify"
);
x
=
purple_xmlnode_new_child
(
error
,
"bad-request"
);
purple_xmlnode_set_namespace
(
x
,
NS_XMPP_STANZAS
);
jabber_iq_send
(
iq
);
}
else
purple_debug_error
(
"jabber"
,
"IQ of type '%s' missing id - ignoring.
\n
"
,
iq_type
);
jabber_id_free
(
from_id
);
return
;
}
signal_return
=
GPOINTER_TO_INT
(
purple_signal_emit_return_1
(
purple_connection_get_protocol
(
js
->
gc
),
"jabber-receiving-iq"
,
js
->
gc
,
iq_type
,
id
,
from
,
packet
));
if
(
signal_return
)
{
jabber_id_free
(
from_id
);
return
;
}
/* First, lets see if a special callback got registered */
if
(
type
==
JABBER_IQ_RESULT
||
type
==
JABBER_IQ_ERROR
)
{
jcd
=
g_hash_table_lookup
(
js
->
iq_callbacks
,
id
);
if
(
jcd
)
{
if
(
does_reply_from_match_request_to
(
js
,
jcd
->
to
,
from_id
))
{
jcd
->
callback
(
js
,
from
,
type
,
id
,
packet
,
jcd
->
data
);
jabber_iq_remove_callback_by_id
(
js
,
id
);
jabber_id_free
(
from_id
);
return
;
}
else
{
char
*
expected_to
;
if
(
jcd
->
to
)
{
expected_to
=
jabber_id_get_full_jid
(
jcd
->
to
);
}
else
{
expected_to
=
jabber_id_get_bare_jid
(
js
->
user
);
}
purple_debug_error
(
"jabber"
,
"Got a result iq with id %s from %s instead of expected %s!
\n
"
,
id
,
from
?
from
:
"(null)"
,
expected_to
);
g_free
(
expected_to
);
}
}
}
/*
* Apparently not, so let's see if we have a pre-defined handler
* or if an outside plugin is interested.
*/
if
(
child
&&
(
xmlns
=
purple_xmlnode_get_namespace
(
child
)))
{
char
*
key
=
g_strdup_printf
(
"%s %s"
,
child
->
name
,
xmlns
);
JabberIqHandler
*
jih
=
g_hash_table_lookup
(
iq_handlers
,
key
);
int
signal_ref
=
GPOINTER_TO_INT
(
g_hash_table_lookup
(
signal_iq_handlers
,
key
));
g_free
(
key
);
if
(
signal_ref
>
0
)
{
signal_return
=
GPOINTER_TO_INT
(
purple_signal_emit_return_1
(
purple_connection_get_protocol
(
js
->
gc
),
"jabber-watched-iq"
,
js
->
gc
,
iq_type
,
id
,
from
,
child
));
if
(
signal_return
)
{
jabber_id_free
(
from_id
);
return
;
}
}
if
(
jih
)
{
jih
(
js
,
from
,
type
,
id
,
child
);
jabber_id_free
(
from_id
);
return
;
}
}
purple_debug_misc
(
"jabber"
,
"Unhandled IQ with id %s
\n
"
,
id
);
/* If we get here, send the default error reply mandated by XMPP-CORE */
if
(
type
==
JABBER_IQ_SET
||
type
==
JABBER_IQ_GET
)
{
JabberIq
*
iq
=
jabber_iq_new
(
js
,
JABBER_IQ_ERROR
);
purple_xmlnode_free
(
iq
->
node
);
iq
->
node
=
purple_xmlnode_copy
(
packet
);
if
(
from
)
{
purple_xmlnode_set_attrib
(
iq
->
node
,
"to"
,
from
);
purple_xmlnode_remove_attrib
(
iq
->
node
,
"from"
);
}
purple_xmlnode_set_attrib
(
iq
->
node
,
"type"
,
"error"
);
error
=
purple_xmlnode_new_child
(
iq
->
node
,
"error"
);
purple_xmlnode_set_attrib
(
error
,
"type"
,
"cancel"
);
purple_xmlnode_set_attrib
(
error
,
"code"
,
"501"
);
x
=
purple_xmlnode_new_child
(
error
,
"feature-not-implemented"
);
purple_xmlnode_set_namespace
(
x
,
NS_XMPP_STANZAS
);
jabber_iq_send
(
iq
);
}
jabber_id_free
(
from_id
);
}
void
jabber_iq_register_handler
(
const
char
*
node
,
const
char
*
xmlns
,
JabberIqHandler
*
handlerfunc
)
{
/*
* This is valid because nodes nor namespaces cannot have spaces in them
* (see http://www.w3.org/TR/2006/REC-xml-20060816/ and
* http://www.w3.org/TR/REC-xml-names/)
*/
char
*
key
=
g_strdup_printf
(
"%s %s"
,
node
,
xmlns
);
g_hash_table_replace
(
iq_handlers
,
key
,
handlerfunc
);
}
void
jabber_iq_signal_register
(
const
gchar
*
node
,
const
gchar
*
xmlns
)
{
gchar
*
key
;
int
ref
;
g_return_if_fail
(
node
!=
NULL
&&
*
node
!=
'\0'
);
g_return_if_fail
(
xmlns
!=
NULL
&&
*
xmlns
!=
'\0'
);
key
=
g_strdup_printf
(
"%s %s"
,
node
,
xmlns
);
ref
=
GPOINTER_TO_INT
(
g_hash_table_lookup
(
signal_iq_handlers
,
key
));
if
(
ref
==
0
)
{
g_hash_table_insert
(
signal_iq_handlers
,
key
,
GINT_TO_POINTER
(
1
));
}
else
{
g_hash_table_insert
(
signal_iq_handlers
,
key
,
GINT_TO_POINTER
(
ref
+
1
));
g_free
(
key
);
}
}
void
jabber_iq_signal_unregister
(
const
gchar
*
node
,
const
gchar
*
xmlns
)
{
gchar
*
key
;
int
ref
;
g_return_if_fail
(
node
!=
NULL
&&
*
node
!=
'\0'
);
g_return_if_fail
(
xmlns
!=
NULL
&&
*
xmlns
!=
'\0'
);
key
=
g_strdup_printf
(
"%s %s"
,
node
,
xmlns
);
ref
=
GPOINTER_TO_INT
(
g_hash_table_lookup
(
signal_iq_handlers
,
key
));
if
(
ref
==
1
)
{
g_hash_table_remove
(
signal_iq_handlers
,
key
);
}
else
if
(
ref
>
1
)
{
g_hash_table_insert
(
signal_iq_handlers
,
key
,
GINT_TO_POINTER
(
ref
-
1
));
}
g_free
(
key
);
}
void
jabber_iq_init
(
void
)
{
iq_handlers
=
g_hash_table_new_full
(
g_str_hash
,
g_str_equal
,
g_free
,
NULL
);
signal_iq_handlers
=
g_hash_table_new_full
(
g_str_hash
,
g_str_equal
,
g_free
,
NULL
);
jabber_iq_register_handler
(
"jingle"
,
JINGLE
,
jingle_parse
);
jabber_iq_register_handler
(
"mailbox"
,
NS_GOOGLE_MAIL_NOTIFY
,
jabber_gmail_poke
);
jabber_iq_register_handler
(
"new-mail"
,
NS_GOOGLE_MAIL_NOTIFY
,
jabber_gmail_poke
);
jabber_iq_register_handler
(
"ping"
,
NS_PING
,
jabber_ping_parse
);
jabber_iq_register_handler
(
"query"
,
NS_GOOGLE_JINGLE_INFO
,
jabber_google_handle_jingle_info
);
jabber_iq_register_handler
(
"query"
,
NS_BYTESTREAMS
,
jabber_bytestreams_parse
);
jabber_iq_register_handler
(
"query"
,
NS_DISCO_INFO
,
jabber_disco_info_parse
);
jabber_iq_register_handler
(
"query"
,
NS_DISCO_ITEMS
,
jabber_disco_items_parse
);
jabber_iq_register_handler
(
"query"
,
NS_LAST_ACTIVITY
,
jabber_iq_last_parse
);
jabber_iq_register_handler
(
"query"
,
NS_OOB_IQ_DATA
,
jabber_oob_parse
);
jabber_iq_register_handler
(
"query"
,
"jabber:iq:register"
,
jabber_register_parse
);
jabber_iq_register_handler
(
"query"
,
"jabber:iq:roster"
,
jabber_roster_parse
);
jabber_iq_register_handler
(
"query"
,
"jabber:iq:version"
,
jabber_iq_version_parse
);
#ifdef USE_VV
jabber_iq_register_handler
(
"session"
,
NS_GOOGLE_SESSION
,
jabber_google_session_parse
);
#endif
jabber_iq_register_handler
(
"block"
,
NS_SIMPLE_BLOCKING
,
jabber_blocklist_parse_push
);
jabber_iq_register_handler
(
"unblock"
,
NS_SIMPLE_BLOCKING
,
jabber_blocklist_parse_push
);
jabber_iq_register_handler
(
"time"
,
NS_ENTITY_TIME
,
jabber_time_parse
);
}
void
jabber_iq_uninit
(
void
)
{
g_hash_table_destroy
(
iq_handlers
);
g_hash_table_destroy
(
signal_iq_handlers
);
iq_handlers
=
signal_iq_handlers
=
NULL
;
}