wireshark/epan/dissectors/packet-gnutella.c
Guy Harris e2089f3d82 Add a URL for Gnutella specs.
Don't check, in the dissectors for particular Gnutella packets, whether
the packet goes past the end of the tvbuff - let that throw an exception
so unreassembled packets are shown as such.

Clean up indentation.

Boost GNUTELLA_MAX_SNAP_SIZE to 4096 (one Gnutella spec suggests that
packets should be < 4K), and use it to

	1) clamp the length of packets (so we don't do huge reassembly
	   or have problems when the total packet length, including the
	   header, overflows 32 bits)

and

	2) check the length of packets before using tcp_dissect_pdus(),
	   to distinguish between packets to be reassembled and a
	   transfer stream.

Pass the correct value to tcp_dissect_pdus() as the header length.

svn path=/trunk/; revision=13211
2005-01-31 00:28:02 +00:00

759 lines
18 KiB
C

/* packet-gnutella.c
* Routines for gnutella dissection
* Copyright 2001, B. Johannessen <bob@havoq.com>
*
* $Id$
*
* Ethereal - Network traffic analyzer
* By Gerald Combs <gerald@ethereal.com>
* Copyright 1998 Gerald Combs
*
* 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <glib.h>
#include <epan/packet.h>
#include "packet-gnutella.h"
#include "packet-tcp.h"
/*
* See
*
* http://rfc-gnutella.sourceforge.net/developer/index.html
*/
static int proto_gnutella = -1;
static int hf_gnutella_stream = -1;
static int hf_gnutella_header = -1;
static int hf_gnutella_header_id = -1;
static int hf_gnutella_header_payload = -1;
static int hf_gnutella_header_ttl = -1;
static int hf_gnutella_header_hops = -1;
static int hf_gnutella_header_size = -1;
static int hf_gnutella_pong_payload = -1;
static int hf_gnutella_pong_port = -1;
static int hf_gnutella_pong_ip = -1;
static int hf_gnutella_pong_files = -1;
static int hf_gnutella_pong_kbytes = -1;
static int hf_gnutella_query_payload = -1;
static int hf_gnutella_query_min_speed = -1;
static int hf_gnutella_query_search = -1;
static int hf_gnutella_queryhit_payload = -1;
static int hf_gnutella_queryhit_count = -1;
static int hf_gnutella_queryhit_port = -1;
static int hf_gnutella_queryhit_ip = -1;
static int hf_gnutella_queryhit_speed = -1;
static int hf_gnutella_queryhit_extra = -1;
static int hf_gnutella_queryhit_servent_id = -1;
static int hf_gnutella_queryhit_hit = -1;
static int hf_gnutella_queryhit_hit_index = -1;
static int hf_gnutella_queryhit_hit_size = -1;
static int hf_gnutella_queryhit_hit_name = -1;
static int hf_gnutella_queryhit_hit_extra = -1;
static int hf_gnutella_push_payload = -1;
static int hf_gnutella_push_servent_id = -1;
static int hf_gnutella_push_index = -1;
static int hf_gnutella_push_ip = -1;
static int hf_gnutella_push_port = -1;
static gint ett_gnutella = -1;
static void dissect_gnutella_pong(tvbuff_t *tvb, guint offset, proto_tree *tree) {
proto_tree_add_item(tree,
hf_gnutella_pong_port,
tvb,
offset + GNUTELLA_PONG_PORT_OFFSET,
GNUTELLA_PORT_LENGTH,
TRUE);
proto_tree_add_item(tree,
hf_gnutella_pong_ip,
tvb,
offset + GNUTELLA_PONG_IP_OFFSET,
GNUTELLA_IP_LENGTH,
FALSE);
proto_tree_add_item(tree,
hf_gnutella_pong_files,
tvb,
offset + GNUTELLA_PONG_FILES_OFFSET,
GNUTELLA_LONG_LENGTH,
TRUE);
proto_tree_add_item(tree,
hf_gnutella_pong_kbytes,
tvb,
offset + GNUTELLA_PONG_KBYTES_OFFSET,
GNUTELLA_LONG_LENGTH,
TRUE);
}
static void dissect_gnutella_query(tvbuff_t *tvb, guint offset, proto_tree *tree, guint size) {
proto_tree_add_item(tree,
hf_gnutella_query_min_speed,
tvb,
offset + GNUTELLA_QUERY_SPEED_OFFSET,
GNUTELLA_SHORT_LENGTH,
TRUE);
if (size > GNUTELLA_SHORT_LENGTH) {
proto_tree_add_item(tree,
hf_gnutella_query_search,
tvb,
offset + GNUTELLA_QUERY_SEARCH_OFFSET,
size - GNUTELLA_SHORT_LENGTH,
FALSE);
}
else {
proto_tree_add_text(tree,
tvb,
offset + GNUTELLA_QUERY_SEARCH_OFFSET,
0,
"Missing data for Query Search.");
}
}
static void dissect_gnutella_queryhit(tvbuff_t *tvb, guint offset, proto_tree *tree, guint size) {
proto_tree *qhi, *hit_tree;
int hit_count, i;
int hit_offset;
int name_length, extra_length;
int idx_at_offset, size_at_offset;
int servent_id_at_offset;
int name_at_offset, extra_at_offset;
int cur_char, remaining, used;
hit_count = tvb_get_guint8(tvb, offset + GNUTELLA_QUERYHIT_COUNT_OFFSET);
proto_tree_add_uint(tree,
hf_gnutella_queryhit_count,
tvb,
offset + GNUTELLA_QUERYHIT_COUNT_OFFSET,
GNUTELLA_BYTE_LENGTH,
hit_count);
proto_tree_add_item(tree,
hf_gnutella_queryhit_port,
tvb,
offset + GNUTELLA_QUERYHIT_PORT_OFFSET,
GNUTELLA_PORT_LENGTH,
TRUE);
proto_tree_add_item(tree,
hf_gnutella_queryhit_ip,
tvb,
offset + GNUTELLA_QUERYHIT_IP_OFFSET,
GNUTELLA_IP_LENGTH,
FALSE);
proto_tree_add_item(tree,
hf_gnutella_queryhit_speed,
tvb,
offset + GNUTELLA_QUERYHIT_SPEED_OFFSET,
GNUTELLA_LONG_LENGTH,
TRUE);
hit_offset = offset + GNUTELLA_QUERYHIT_FIRST_HIT_OFFSET;
for(i = 0; i < hit_count; i++) {
idx_at_offset = hit_offset;
size_at_offset = hit_offset + GNUTELLA_QUERYHIT_HIT_SIZE_OFFSET;
hit_offset += (GNUTELLA_LONG_LENGTH * 2);
name_length = 0;
extra_length = 0;
name_at_offset = hit_offset;
while(hit_offset - offset < size) {
cur_char = tvb_get_guint8(tvb, hit_offset);
if(cur_char == '\0')
break;
hit_offset++;
name_length++;
}
hit_offset++;
extra_at_offset = hit_offset;
while(hit_offset - offset < size) {
cur_char = tvb_get_guint8(tvb, hit_offset);
if(cur_char == '\0')
break;
hit_offset++;
extra_length++;
}
hit_offset++;
qhi = proto_tree_add_item(tree,
hf_gnutella_queryhit_hit,
tvb,
idx_at_offset,
(GNUTELLA_LONG_LENGTH * 2) +
name_length + extra_length +
GNUTELLA_QUERYHIT_END_OF_STRING_LENGTH,
FALSE);
hit_tree = proto_item_add_subtree(qhi, ett_gnutella);
proto_tree_add_item(hit_tree,
hf_gnutella_queryhit_hit_index,
tvb,
idx_at_offset,
GNUTELLA_LONG_LENGTH,
TRUE);
proto_tree_add_item(hit_tree,
hf_gnutella_queryhit_hit_size,
tvb,
size_at_offset,
GNUTELLA_LONG_LENGTH,
TRUE);
proto_tree_add_item(hit_tree,
hf_gnutella_queryhit_hit_name,
tvb,
name_at_offset,
name_length,
FALSE);
if(extra_length) {
proto_tree_add_item(hit_tree,
hf_gnutella_queryhit_hit_extra,
tvb,
extra_at_offset,
extra_length,
FALSE);
}
}
used = hit_offset - offset;
remaining = size - used;
if(remaining > GNUTELLA_SERVENT_ID_LENGTH) {
servent_id_at_offset = hit_offset + remaining - GNUTELLA_SERVENT_ID_LENGTH;
proto_tree_add_item(tree,
hf_gnutella_queryhit_extra,
tvb,
hit_offset,
servent_id_at_offset - hit_offset,
FALSE);
}
else {
servent_id_at_offset = hit_offset;
}
proto_tree_add_item(tree,
hf_gnutella_queryhit_servent_id,
tvb,
servent_id_at_offset,
GNUTELLA_SERVENT_ID_LENGTH,
FALSE);
}
static void dissect_gnutella_push(tvbuff_t *tvb, guint offset, proto_tree *tree) {
proto_tree_add_item(tree,
hf_gnutella_push_servent_id,
tvb,
offset + GNUTELLA_PUSH_SERVENT_ID_OFFSET,
GNUTELLA_SERVENT_ID_LENGTH,
FALSE);
proto_tree_add_item(tree,
hf_gnutella_push_index,
tvb,
offset + GNUTELLA_PUSH_INDEX_OFFSET,
GNUTELLA_LONG_LENGTH,
TRUE);
proto_tree_add_item(tree,
hf_gnutella_push_ip,
tvb,
offset + GNUTELLA_PUSH_IP_OFFSET,
GNUTELLA_IP_LENGTH,
FALSE);
proto_tree_add_item(tree,
hf_gnutella_push_port,
tvb,
offset + GNUTELLA_PUSH_PORT_OFFSET,
GNUTELLA_PORT_LENGTH,
TRUE);
}
static guint
get_gnutella_pdu_len(tvbuff_t *tvb, int offset) {
guint32 size;
size = tvb_get_letohl(
tvb,
offset + GNUTELLA_HEADER_SIZE_OFFSET);
if (size > GNUTELLA_MAX_SNAP_SIZE) {
/*
* XXX - arbitrary limit, preventing overflows and
* attempts to reassemble 4GB of data.
*/
size = GNUTELLA_MAX_SNAP_SIZE;
}
/* The size doesn't include the header */
return GNUTELLA_HEADER_LENGTH + size;
}
static void dissect_gnutella_pdu(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree) {
proto_item *ti, *hi, *pi;
proto_tree *gnutella_tree = NULL;
proto_tree *gnutella_header_tree, *gnutella_pong_tree;
proto_tree *gnutella_queryhit_tree, *gnutella_push_tree;
proto_tree *gnutella_query_tree;
guint8 payload_descriptor;
guint32 size = 0;
char *payload_descriptor_text;
if (tree) {
ti = proto_tree_add_item(tree,
proto_gnutella,
tvb,
0,
-1,
FALSE);
gnutella_tree = proto_item_add_subtree(ti, ett_gnutella);
size = tvb_get_letohl(
tvb,
GNUTELLA_HEADER_SIZE_OFFSET);
}
payload_descriptor = tvb_get_guint8(
tvb,
GNUTELLA_HEADER_PAYLOAD_OFFSET);
switch(payload_descriptor) {
case GNUTELLA_PING:
payload_descriptor_text = GNUTELLA_PING_NAME;
break;
case GNUTELLA_PONG:
payload_descriptor_text = GNUTELLA_PONG_NAME;
break;
case GNUTELLA_PUSH:
payload_descriptor_text = GNUTELLA_PUSH_NAME;
break;
case GNUTELLA_QUERY:
payload_descriptor_text = GNUTELLA_QUERY_NAME;
break;
case GNUTELLA_QUERYHIT:
payload_descriptor_text = GNUTELLA_QUERYHIT_NAME;
break;
default:
payload_descriptor_text = GNUTELLA_UNKNOWN_NAME;
break;
}
if (check_col(pinfo->cinfo, COL_INFO))
col_append_sep_fstr(pinfo->cinfo, COL_INFO, NULL, "%s",
payload_descriptor_text);
if (tree) {
hi = proto_tree_add_item(gnutella_tree,
hf_gnutella_header,
tvb,
0,
GNUTELLA_HEADER_LENGTH,
FALSE);
gnutella_header_tree = proto_item_add_subtree(hi, ett_gnutella);
proto_tree_add_item(gnutella_header_tree,
hf_gnutella_header_id,
tvb,
GNUTELLA_HEADER_ID_OFFSET,
GNUTELLA_SERVENT_ID_LENGTH,
FALSE);
proto_tree_add_uint_format(gnutella_header_tree,
hf_gnutella_header_payload,
tvb,
GNUTELLA_HEADER_PAYLOAD_OFFSET,
GNUTELLA_BYTE_LENGTH,
payload_descriptor,
"Payload: %i (%s)",
payload_descriptor,
payload_descriptor_text);
proto_tree_add_item(gnutella_header_tree,
hf_gnutella_header_ttl,
tvb,
GNUTELLA_HEADER_TTL_OFFSET,
GNUTELLA_BYTE_LENGTH,
FALSE);
proto_tree_add_item(gnutella_header_tree,
hf_gnutella_header_hops,
tvb,
GNUTELLA_HEADER_HOPS_OFFSET,
GNUTELLA_BYTE_LENGTH,
FALSE);
proto_tree_add_uint(gnutella_header_tree,
hf_gnutella_header_size,
tvb,
GNUTELLA_HEADER_SIZE_OFFSET,
GNUTELLA_LONG_LENGTH,
size);
if (size > 0) {
switch(payload_descriptor) {
case GNUTELLA_PONG:
pi = proto_tree_add_item(
gnutella_header_tree,
hf_gnutella_pong_payload,
tvb,
GNUTELLA_HEADER_LENGTH,
size,
FALSE);
gnutella_pong_tree = proto_item_add_subtree(
pi,
ett_gnutella);
dissect_gnutella_pong(
tvb,
GNUTELLA_HEADER_LENGTH,
gnutella_pong_tree);
break;
case GNUTELLA_PUSH:
pi = proto_tree_add_item(
gnutella_header_tree,
hf_gnutella_push_payload,
tvb,
GNUTELLA_HEADER_LENGTH,
size,
FALSE);
gnutella_push_tree = proto_item_add_subtree(
pi,
ett_gnutella);
dissect_gnutella_push(
tvb,
GNUTELLA_HEADER_LENGTH,
gnutella_push_tree);
break;
case GNUTELLA_QUERY:
pi = proto_tree_add_item(
gnutella_header_tree,
hf_gnutella_query_payload,
tvb,
GNUTELLA_HEADER_LENGTH,
size,
FALSE);
gnutella_query_tree = proto_item_add_subtree(
pi,
ett_gnutella);
dissect_gnutella_query(
tvb,
GNUTELLA_HEADER_LENGTH,
gnutella_query_tree,
size);
break;
case GNUTELLA_QUERYHIT:
pi = proto_tree_add_item(
gnutella_header_tree,
hf_gnutella_queryhit_payload,
tvb,
GNUTELLA_HEADER_LENGTH,
size,
FALSE);
gnutella_queryhit_tree = proto_item_add_subtree(
pi,
ett_gnutella);
dissect_gnutella_queryhit(
tvb,
GNUTELLA_HEADER_LENGTH,
gnutella_queryhit_tree,
size);
break;
}
}
}
}
static void dissect_gnutella(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree) {
proto_item *ti;
proto_tree *gnutella_tree = NULL;
guint32 size;
if (check_col(pinfo->cinfo, COL_PROTOCOL))
col_set_str(pinfo->cinfo, COL_PROTOCOL, "Gnutella");
if (check_col(pinfo->cinfo, COL_INFO))
col_clear(pinfo->cinfo, COL_INFO);
/*
* OK, do we have enough data to determine whether this
* is Gnutella messages or just a transfer stream?
*/
if (tvb_bytes_exist(tvb, GNUTELLA_HEADER_SIZE_OFFSET, 4)) {
/*
* Yes - fetch the length and see if it's bigger
* than GNUTELLA_MAX_SNAP_SIZE; if it is, we assume
* it's a transfer stream.
*
* Should we also check the payload descriptor?
*/
size = tvb_get_letohl(
tvb,
GNUTELLA_HEADER_SIZE_OFFSET);
if (size > GNUTELLA_MAX_SNAP_SIZE) {
if (tree) {
ti = proto_tree_add_item(tree,
proto_gnutella,
tvb,
0,
-1,
FALSE);
gnutella_tree = proto_item_add_subtree(ti,
ett_gnutella);
proto_tree_add_item(gnutella_tree,
hf_gnutella_stream,
tvb,
0,
-1,
FALSE);
}
return;
}
}
tcp_dissect_pdus(tvb, pinfo, tree, TRUE, GNUTELLA_HEADER_SIZE_OFFSET+4,
get_gnutella_pdu_len, dissect_gnutella_pdu);
}
void proto_register_gnutella(void) {
static hf_register_info hf[] = {
{ &hf_gnutella_header,
{ "Descriptor Header", "gnutella.header",
FT_NONE, BASE_NONE, NULL, 0,
"Gnutella Descriptor Header", HFILL }
},
{ &hf_gnutella_pong_payload,
{ "Pong", "gnutella.pong.payload",
FT_NONE, BASE_NONE, NULL, 0,
"Gnutella Pong Payload", HFILL }
},
{ &hf_gnutella_push_payload,
{ "Push", "gnutella.push.payload",
FT_NONE, BASE_NONE, NULL, 0,
"Gnutella Push Payload", HFILL }
},
{ &hf_gnutella_query_payload,
{ "Query", "gnutella.query.payload",
FT_NONE, BASE_NONE, NULL, 0,
"Gnutella Query Payload", HFILL }
},
{ &hf_gnutella_queryhit_payload,
{ "QueryHit", "gnutella.queryhit.payload",
FT_NONE, BASE_NONE, NULL, 0,
"Gnutella QueryHit Payload", HFILL }
},
{ &hf_gnutella_stream,
{ "Gnutella Upload / Download Stream", "gnutella.stream",
FT_NONE, BASE_NONE, NULL, 0,
"Gnutella Upload / Download Stream", HFILL }
},
{ &hf_gnutella_header_id,
{ "ID", "gnutella.header.id",
FT_BYTES, BASE_HEX, NULL, 0,
"Gnutella Descriptor ID", HFILL }
},
{ &hf_gnutella_header_payload,
{ "Payload", "gnutella.header.payload",
FT_UINT8, BASE_DEC, NULL, 0,
"Gnutella Descriptor Payload", HFILL }
},
{ &hf_gnutella_header_ttl,
{ "TTL", "gnutella.header.ttl",
FT_UINT8, BASE_DEC, NULL, 0,
"Gnutella Descriptor Time To Live", HFILL }
},
{ &hf_gnutella_header_hops,
{ "Hops", "gnutella.header.hops",
FT_UINT8, BASE_DEC, NULL, 0,
"Gnutella Descriptor Hop Count", HFILL }
},
{ &hf_gnutella_header_size,
{ "Length", "gnutella.header.size",
FT_UINT8, BASE_DEC, NULL, 0,
"Gnutella Descriptor Payload Length", HFILL }
},
{ &hf_gnutella_pong_port,
{ "Port", "gnutella.pong.port",
FT_UINT16, BASE_DEC, NULL, 0,
"Gnutella Pong TCP Port", HFILL }
},
{ &hf_gnutella_pong_ip,
{ "IP", "gnutella.pong.ip",
FT_IPv4, BASE_DEC, NULL, 0,
"Gnutella Pong IP Address", HFILL }
},
{ &hf_gnutella_pong_files,
{ "Files Shared", "gnutella.pong.files",
FT_UINT32, BASE_DEC, NULL, 0,
"Gnutella Pong Files Shared", HFILL }
},
{ &hf_gnutella_pong_kbytes,
{ "KBytes Shared", "gnutella.pong.kbytes",
FT_UINT32, BASE_DEC, NULL, 0,
"Gnutella Pong KBytes Shared", HFILL }
},
{ &hf_gnutella_query_min_speed,
{ "Min Speed", "gnutella.query.min_speed",
FT_UINT32, BASE_DEC, NULL, 0,
"Gnutella Query Minimum Speed", HFILL }
},
{ &hf_gnutella_query_search,
{ "Search", "gnutella.query.search",
FT_STRINGZ, BASE_NONE, NULL, 0,
"Gnutella Query Search", HFILL }
},
{ &hf_gnutella_queryhit_hit,
{ "Hit", "gnutella.queryhit.hit",
FT_NONE, BASE_NONE, NULL, 0,
"Gnutella QueryHit", HFILL }
},
{ &hf_gnutella_queryhit_hit_index,
{ "Index", "gnutella.queryhit.hit.index",
FT_UINT32, BASE_DEC, NULL, 0,
"Gnutella QueryHit Index", HFILL }
},
{ &hf_gnutella_queryhit_hit_size,
{ "Size", "gnutella.queryhit.hit.size",
FT_UINT32, BASE_DEC, NULL, 0,
"Gnutella QueryHit Size", HFILL }
},
{ &hf_gnutella_queryhit_hit_name,
{ "Name", "gnutella.queryhit.hit.name",
FT_STRING, BASE_NONE, NULL, 0,
"Gnutella Query Name", HFILL }
},
{ &hf_gnutella_queryhit_hit_extra,
{ "Extra", "gnutella.queryhit.hit.extra",
FT_BYTES, BASE_HEX, NULL, 0,
"Gnutella Query Extra", HFILL }
},
{ &hf_gnutella_queryhit_count,
{ "Count", "gnutella.queryhit.count",
FT_UINT8, BASE_DEC, NULL, 0,
"Gnutella QueryHit Count", HFILL }
},
{ &hf_gnutella_queryhit_port,
{ "Port", "gnutella.queryhit.port",
FT_UINT16, BASE_DEC, NULL, 0,
"Gnutella QueryHit Port", HFILL }
},
{ &hf_gnutella_queryhit_ip,
{ "IP", "gnutella.queryhit.ip",
FT_IPv4, BASE_DEC, NULL, 0,
"Gnutella QueryHit IP Address", HFILL }
},
{ &hf_gnutella_queryhit_speed,
{ "Speed", "gnutella.queryhit.speed",
FT_UINT32, BASE_DEC, NULL, 0,
"Gnutella QueryHit Speed", HFILL }
},
{ &hf_gnutella_queryhit_extra,
{ "Extra", "gnutella.queryhit.extra",
FT_BYTES, BASE_HEX, NULL, 0,
"Gnutella QueryHit Extra", HFILL }
},
{ &hf_gnutella_queryhit_servent_id,
{ "Servent ID", "gnutella.queryhit.servent_id",
FT_BYTES, BASE_HEX, NULL, 0,
"Gnutella QueryHit Servent ID", HFILL }
},
{ &hf_gnutella_push_servent_id,
{ "Servent ID", "gnutella.push.servent_id",
FT_BYTES, BASE_HEX, NULL, 0,
"Gnutella Push Servent ID", HFILL }
},
{ &hf_gnutella_push_ip,
{ "IP", "gnutella.push.ip",
FT_IPv4, BASE_DEC, NULL, 0,
"Gnutella Push IP Address", HFILL }
},
{ &hf_gnutella_push_index,
{ "Index", "gnutella.push.index",
FT_UINT32, BASE_DEC, NULL, 0,
"Gnutella Push Index", HFILL }
},
{ &hf_gnutella_push_port,
{ "Port", "gnutella.push.port",
FT_UINT16, BASE_DEC, NULL, 0,
"Gnutella Push Port", HFILL }
},
};
static gint *ett[] = {
&ett_gnutella,
};
proto_gnutella = proto_register_protocol("Gnutella Protocol",
"GNUTELLA",
"gnutella");
proto_register_field_array(proto_gnutella, hf, array_length(hf));
proto_register_subtree_array(ett, array_length(ett));
}
void proto_reg_handoff_gnutella(void) {
dissector_handle_t gnutella_handle;
gnutella_handle = create_dissector_handle(dissect_gnutella,
proto_gnutella);
dissector_add("tcp.port", GNUTELLA_TCP_PORT, gnutella_handle);
}