diff options
Diffstat (limited to 'hicn-light/src/hicn/core/connection.c')
-rw-r--r-- | hicn-light/src/hicn/core/connection.c | 720 |
1 files changed, 478 insertions, 242 deletions
diff --git a/hicn-light/src/hicn/core/connection.c b/hicn-light/src/hicn/core/connection.c index c2ac71a5f..582e2f56f 100644 --- a/hicn-light/src/hicn/core/connection.c +++ b/hicn-light/src/hicn/core/connection.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2019 Cisco and/or its affiliates. + * Copyright (c) 2017-2020 Cisco and/or its affiliates. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at: @@ -13,347 +13,583 @@ * limitations under the License. */ -#include <limits.h> -#include <hicn/hicn-light/config.h> -#include <stdio.h> +/** + * @file connection.c + * @brief Implementation of hICN connections + */ -#include <hicn/core/connection.h> -#include <hicn/core/connectionState.h> -#include <hicn/core/messageHandler.h> -#include <hicn/core/ticks.h> -#include <hicn/core/wldr.h> -#include <hicn/io/addressPair.h> -#include <hicn/io/ioOperations.h> +#include <assert.h> -#include <parc/algol/parc_Memory.h> -#include <parc/assert/parc_Assert.h> -#ifdef WITH_POLICY -#include <hicn/policy.h> -#endif /* WITH_POLICY */ +#include <hicn/core/forwarder.h> +#include <hicn/util/log.h> +#include <hicn/core/wldr.h> -struct connection { +#include "connection.h" +#include "connection_vft.h" - const AddressPair *addressPair; - IoOperations *ops; +#define _conn_var(x) _connection_ ## x - unsigned refCount; +#if 0 - unsigned counter; +/* Accessors */ - bool wldrAutoStart; // if true, wldr can be set automatically - // by default this value is set to true. - // if wldr is activated using a command (config - // file/hicnLightControl) this value is set to false so - // that a base station can not disable wldr at the client - Wldr *wldr; +static inline +unsigned +connection_get_id(const connection_t * connection) +{ + return connection->id; +} -#ifdef WITH_POLICY - policy_tags_t tags; -#endif /* WITH_POLICY */ +static inline +char * +connection_get_name(const connection_t * connection) +{ + return connection->name; +} -}; +static inline +face_type_t +connection_get_type(const connection_t * connection) +{ + return connection->type; +} -Connection *connection_Create(IoOperations *ops) { - parcAssertNotNull(ops, "Parameter ops must be non-null"); - Connection *conn = parcMemory_AllocateAndClear(sizeof(Connection)); - parcAssertNotNull(conn, "parcMemory_AllocateAndClear(%zu) returned NULL", - sizeof(Connection)); - conn->addressPair = ioOperations_GetAddressPair(ops); - conn->ops = ops; - conn->refCount = 1; - conn->wldr = NULL; +static inline +address_pair_t * +connection_get_pair(const connection_t * connection) +{ + return connection->pair; +} - conn->wldrAutoStart = true; - conn->counter = 0; +static inline +bool +connection_is_up(const connection_t * connection) +{ + return connection->up; +} - /* By default, a connection will aim at the UP state */ - connection_SetAdminState(conn, CONNECTION_STATE_UP); +static inline +bool +connection_is_local(const connection_t * connection) +{ + return connection->local; +} -#ifdef WITH_POLICY - conn->tags = POLICY_TAGS_EMPTY; -#endif /* WITH_POLICY */ +static inline +face_state_t +connection_get_state(const connection_t * connection) +{ + return connection->state; +} - return conn; +static inline +void +connection_set_state(connection_t * connection, face_state_t state) +{ + connection->state = state; } -Connection *connection_Acquire(Connection *connection) { - parcAssertNotNull(connection, "Parameter conn must be non-null"); - connection->refCount++; - return connection; +static inline +face_state_t +connection_get_admin_state(const connection_t * connection) +{ + return connection->admin_state; } -void connection_Release(Connection **connectionPtr) { - parcAssertNotNull(connectionPtr, "Parameter must be non-null double pointer"); - parcAssertNotNull(*connectionPtr, - "Parameter must dereference to non-null pointer"); - Connection *conn = *connectionPtr; +static inline +void +connection_set_admin_state(connection_t * connection, face_state_t state) +{ + connection->admin_state = state; +} - parcAssertTrue( - conn->refCount > 0, - "Invalid state, connection reference count should be positive, got 0."); - conn->refCount--; - if (conn->refCount == 0) { - // don't destroy addressPair, its part of ops. - ioOperations_Release(&conn->ops); - if (conn->wldr != NULL) { - wldr_Destroy(&(conn->wldr)); - } - parcMemory_Deallocate((void **)&conn); - } - *connectionPtr = NULL; +static inline +const char * +connection_get_interface_name(const connection_t * connection) +{ + return connection->interface_name; } -bool connection_Send(const Connection *conn, Message *message) { - parcAssertNotNull(conn, "Parameter conn must be non-null"); - parcAssertNotNull(message, "Parameter message must be non-null"); +#ifdef WITH_POLICY - if (ioOperations_IsUp(conn->ops)) { - if (message_GetType(message) == MessagePacketType_ContentObject) { - uint8_t connectionId = (uint8_t)connection_GetConnectionId(conn); - message_UpdatePathLabel(message, connectionId); - } - if (conn->wldr != NULL) { - wldr_SetLabel(conn->wldr, message); - } else { - message_ResetWldrLabel(message); - } - return ioOperations_Send(conn->ops, NULL, message); - } - return false; +static inline +uint32_t +connection_get_priority(const connection_t * connection) +{ + connection->priority = priority; } -bool connection_SendIOVBuffer(const Connection *conn, struct iovec *msg, - size_t size) { - parcAssertNotNull(conn, "Parameter conn must be non-null"); - parcAssertNotNull(msg, "Parameter message must be non-null"); - - return ioOperations_SendIOVBuffer(conn->ops, msg, size); +static inline +void +connection_set_priority(connection_t * connection, uint32_t priority) +{ + connection->priority = priority; } -bool connection_SendBuffer(const Connection *conn, u8 * buffer, size_t length) +static inline +policy_tags_t +connection_get_tags(const connection_t * connection) { - struct iovec iov[1]; - iov[0].iov_base = buffer; - iov[0].iov_len = length; - return connection_SendIOVBuffer(conn, iov, 1); + return connection->tags; } -void connection_Probe(Connection *conn, uint8_t * probe) { - ioOperations_SendProbe(conn->ops, probe); +static inline +void +connection_set_tags(connection_t * connection, policy_tags_t tags) +{ + connection->tags = tags; } -void connection_HandleProbe(Connection *conn, uint8_t *probe){ - parcAssertNotNull(conn, "Parameter conn must be non-null"); - parcAssertNotNull(probe, "Parameter pkt must be non-null"); +#endif /* WITH_POLICY */ - if(messageHandler_IsInterest(probe)){ - messageHandler_CreateProbeReply(probe, HF_INET6_TCP); - ioOperations_SendProbe(conn->ops, probe); - } -} +/* API */ -IoOperations *connection_GetIoOperations(const Connection *conn) { - return conn->ops; -} +#endif -unsigned connection_GetConnectionId(const Connection *conn) { - parcAssertNotNull(conn, "Parameter conn must be non-null"); - return ioOperations_GetConnectionId(conn->ops); -} +connection_t * +connection_create_on_listener(const listener_t * listener, const char * name, + const address_pair_t * pair, forwarder_t * forwarder) +{ + const connection_table_t * table = forwarder_get_connection_table(forwarder); -const AddressPair *connection_GetAddressPair(const Connection *conn) { - parcAssertNotNull(conn, "Parameter conn must be non-null"); - return ioOperations_GetAddressPair(conn->ops); -} + connection_t * connection; + connection_table_allocate(table, connection, pair, name); -bool connection_IsUp(const Connection *conn) { - parcAssertNotNull(conn, "Parameter conn must be non-null"); - if (!conn->ops) return false; - return ioOperations_IsUp(conn->ops); + unsigned connection_id = connection_table_get_connection_id(table, connection); + + const char * interface_name = listener_get_interface_name(listener); + // XXX This should not be there ! + int fd = listener_get_socket(listener, address_pair_get_local(pair), + address_pair_get_remote(pair), NULL); + bool local = address_is_local(&pair->local); + + if (connection_initialize(connection, listener->type, name, interface_name, fd, pair, local, + connection_id, forwarder) < 0) { + connection_table_deallocate(table, connection); + return NULL; + } + + return connection; } -bool connection_IsLocal(const Connection *conn) { - parcAssertNotNull(conn, "Parameter conn must be non-null"); - return ioOperations_IsLocal(conn->ops); + +// This is called by configuration +// XXX different wit create on listener : when listener receives a new +// connection +// ! +connection_t * +connection_create(face_type_t type, const char * name, + const address_pair_t * pair, forwarder_t * forwarder) +{ + assert(face_type_is_valid(type)); + assert(pair); + assert(forwarder); + + listener_table_t * table = forwarder_get_listener_table(forwarder); + listener_t *listener = listener_table_get_by_address(table, type, &pair->local); + if (!listener) { + // XXX TODO + //char *str = addressToString(localAddress); + ERROR("Could not find listener to match address N/A"); + //parcMemory_Deallocate((void **)&str); + return NULL; + } + + return connection_create_on_listener(listener, name, pair, forwarder); } -const void *connection_Class(const Connection *conn) { - parcAssertNotNull(conn, "Parameter conn must be non-null"); - return ioOperations_Class(conn->ops); +#if 0 + + const char * interface_name = listener->getInterfaceName(listener); + int fd = listener->getSocket(listener, pair); + bool is_local = address_is_local(&pair->local); + + + return udpConnection_Create(forwarder, interface_name, fd, pair, is_local, connid); + + // alternatively + // } +#endif -bool connection_ReSend(const Connection *conn, Message *message, - bool notification) { - parcAssertNotNull(conn, "Parameter conn must be non-null"); - parcAssertNotNull(message, "Parameter message must be non-null"); - bool res = false; +/** + * @brief Initializes a connection + * + * @param [out] connection - Allocated connection buffer (eg. from pool) to be + * initialized. + * @param [in] forwarder - forwarder_t to which the connection is associated. This + * parameter needs to be non-NULL for connections receiving packets, such + * as TCP connections which are very close to UDP listeners, and unlike + * bound UDP connections). + * @return 0 if no error, -1 otherwise + */ +int +connection_initialize(connection_t * connection, face_type_t type, const char * name, + const char * interface_name, int fd, const address_pair_t * pair, + bool local, unsigned connection_id, forwarder_t * forwarder) +{ + int rc; + + assert(connection); + /* Interface name can be NULL eg always for TCP connnections */ + assert(pair); + assert(address_pair_valid(pair)); + + *connection = (connection_t) { + .id = connection_id, + .name = strdup(name), + .type = type, + .interface_name = strdup(interface_name), + .pair = *pair, + .fd = fd, +// .up = true, + .local = local, + // XXX UDP should start UP, TCP DOWN until remove side answer ? + .state = FACE_STATE_UNDEFINED, + .admin_state = FACE_STATE_UP, +#ifdef WITH_POLICY + .priority = 0, +#endif /* WITH_POLICY */ - if (connection_IsUp(conn)) { - // here the wldr header is alreay set: this message is a retransmission or a - // notification + .forwarder = forwarder, + .closed = false, - // we need to recompiute the path lable since we always store a pointer to - // the same message if this message will be sent again to someonelse, the - // new path label must be computed starting from the orignal labelorignal - // label. Notice that we heve the same problem in case of PIT aggregation. - // That case is handled insied the MessageProcessor. This is specific to - // WLDR retransmittions. This is done only for data packets + /* WLDR */ + .wldr = NULL, + .wldr_autostart = true, + }; - if (message_GetType(message) == MessagePacketType_ContentObject) { - uint8_t connectionId = (uint8_t)connection_GetConnectionId(conn); - uint32_t old_path_label = message_GetPathLabel(message); - message_UpdatePathLabel(message, connectionId); + connection->data = malloc(connection_vft[connection->type]->data_size); + if (!connection->data) + goto ERR_DATA; - res = ioOperations_Send(conn->ops, NULL, message); + assert(connection_has_valid_type(connection)); - message_SetPathLabel(message, old_path_label); - } else { - res = ioOperations_Send(conn->ops, NULL, message); + rc = connection_vft[connection->type]->initialize(connection); + if (rc < 0) { + goto ERR_VFT; } - } - if (notification) { - // the notification is never destroyed - message_Release(&message); - } + // XXX uncertain as fd is created by the listener !! + // XXX check whether it is registered ! +#if 0 + connection->fd = connection_vft[connection->type]->get_socket(connection, address, NULL, interface_name); + if (connection->fd < 0) { + ERROR("Error creating connection fd: (%d) %s", errno, strerror(errno)); + goto ERR_FD; + } + + // XXX data should be pre-allocated here + + if (loop_register_fd(MAIN_LOOP, connection->fd, connection, + connection_vft[connection->type]->read_callback, NULL) < 0) + goto ERR_REGISTER_FD; +#endif + + // XXX TODO + //char *str = pair_ToString(udp->pair); + DEBUG("%s connection %p created for address %s (local=%s)", + face_type_str(connection->type), connection, "N/A", + connection_is_local(connection) ? "true" : "false"); + //free(str); + // + return 0; + +#if 0 +ERR_REGISTER_FD: +#ifndef _WIN32 + close(connection->fd); +#else + closesocket(connection->fd); +#endif +ERR_FD: +#endif +ERR_VFT: + free(connection->data); +ERR_DATA: + free(connection->interface_name); + free(connection->name); + return -1; +} + +int +connection_finalize(connection_t * connection) +{ + assert(connection); - return res; + if (connection->wldr) + wldr_free(connection->wldr); + + connection_vft[connection->type]->finalize(connection); + + free(connection->interface_name); + free(connection); + + DEBUG("%s connection %p destroyed", face_type_str(connection->type), + connection); + + return 0; } -void connection_AllowWldrAutoStart(Connection *conn, bool allow) { - conn->wldrAutoStart = allow; +#if 0 +// XXX put in common the validation and processing of commands with UDP and hICN +// listeners ! +command_type_t +_isACommand(PARCEventBuffer *input) +{ + size_t bytesAvailable = parcEventBuffer_GetLength(input); + parcAssertTrue(bytesAvailable >= sizeof(header_control_message), + "Called with too short an input: %zu", bytesAvailable); + + uint8_t *msg = parcEventBuffer_Pullup(input, bytesAvailable); + + message_type_t message_type = message_type_from_uchar(msg[0]); + //if (!message_type_is_valid(message_type)) + if (message_type != REQUEST_LIGHT) + return COMMAND_TYPE_N; + + return command_type_from_uchar(msg[1]); } -void connection_EnableWldr(Connection *conn) { - if (!connection_IsLocal(conn)) { - if (conn->wldr == NULL) { - printf("----------------- enable wldr\n"); - conn->wldr = wldr_Init(); +// XXX new function to process all incoming bytes +// result : consumed, discard/invalid, wait for more +// PRE: buffer has at least 8 bytes (to get the length of all packets) +// This function is only used to make decisions +/** + * \return the number of consumed bytes, or a negative value in case of error + */ +// XXX mutualize with listener_process_buffer +size_t +connection_process_buffer(connection_t * connection, const uint8_t * buffer, size_t size) +{ + size_t expected; + + /* Too small a packet is not useful to decide between a control message and + * an hICN packet, the size of a control message is enough to test for both + * pakcet types */ + if (size < sizeof(header_control_message)) + return 0; + + /* We expect complete packets most of the time, so don't bother with state */ + message_type_t message_type = message_type_from_uchar(msg[0]); + if (message_type == REQUEST_LIGHT) { + command_type_t command_type = command_type_from_uchar(msg[1]); + if (!command_type_is_valid(command_type)) + break; + expected = sizeof(header_control_message) + + command_get_payload_len(command_type); + if (size < expected) + return 0; + forwarder_receive_command(connection->forwarder, command_type, packet, + connection->id); + return expected; + } + + if (!messageHandler_IsValidHicnPacket(packet)) { + WARN("Connection #%u: Malformed packet received", + connection_get_id(connection)); + return -1; } - } -} -void connection_DisableWldr(Connection *conn) { - if (!connection_IsLocal(conn)) { - if (conn->wldr != NULL) { - printf("----------------- disable wldr\n"); - wldr_Destroy(&(conn->wldr)); - conn->wldr = NULL; + /* Check that we have a full packet */ + expected = messageHandler_GetTotalPacketLength(packet), + if (size < expected) + return 0; + + msgbuf_t msgbuf; + MessagePacketType packet_type; + if (messageHandler_IsInterest(message->messageHead)) { + packet_type = MESSAGE_TYPE_INTEREST; + } else if (messageHandler_IsData(message->messageHead)) { + packet_type = MESSAGE_TYPE_DATA; + } else { + ERROR("Dropped packet that is not interest nor data"); + return -1; } - } -} -bool connection_HasWldr(const Connection *conn) { - if (conn->wldr == NULL) { - return false; - } else { - return true; - } -} + // this is an Hicn packet (here we should distinguish between IPv4 and + // IPv6 tryReadMessage may set nextMessageLength + msgbuf_from_packet(&msgbuf, packet, expected, packet_type, + connection_get_id(connection), ticks_now()); + forwarder_receive(connection->forwarder, &msgbuf, 1); -bool connection_WldrAutoStartAllowed(const Connection *conn) { - return conn->wldrAutoStart; + return size; } -void connection_DetectLosses(Connection *conn, Message *message) { - if (conn->wldr != NULL) wldr_DetectLosses(conn->wldr, conn, message); -} +int +connection_read_message(connection_t * connection, msgbuf_t * msgbuf) +{ + assert(connection); + assert(face_type_is_valid(connection->type)); + assert(msgbuf); -void connection_HandleWldrNotification(Connection *conn, Message *message) { - if (conn->wldr != NULL) - wldr_HandleWldrNotification(conn->wldr, conn, message); + return connection_vft[connection->type]->read_message(connection, msgbuf); } -connection_state_t connection_GetState(const Connection *conn) +uint8_t * +connection_read_packet(connection_t * connection) { - parcAssertNotNull(conn, "Parameter conn must be non-null"); - if (!conn->ops) - return CONNECTION_STATE_UNDEFINED; - return ioOperations_GetState(conn->ops); + assert(connection); + assert(face_type_is_valid(connection->type)); + + return connection_vft[connection->type]->read_packet(connection); } +#endif -void connection_SetState(Connection *conn, connection_state_t state) +int +connection_send_packet(const connection_t * connection, const uint8_t * packet, + size_t size) { - parcAssertNotNull(conn, "Parameter conn must be non-null"); - if (!conn->ops) - return; - ioOperations_SetState(conn->ops, state); + assert(connection); + assert(face_type_is_valid(connection->type)); + assert(packet); + + return connection_vft[connection->type]->send_packet(connection, packet, size); } -connection_state_t connection_GetAdminState(const Connection *conn) +// ALL DEPRECATED CODE HERE TO BE UPDATED + +// XXX nexthops null ?? to be removed ??? +bool +_connection_send(const connection_t * connection, msgbuf_t * msgbuf, bool queue) { - parcAssertNotNull(conn, "Parameter conn must be non-null"); - if (!conn->ops) - return CONNECTION_STATE_UNDEFINED; - return ioOperations_GetAdminState(conn->ops); + return connection_vft[connection->type]->send(connection, msgbuf, queue); } -void connection_SetAdminState(Connection *conn, connection_state_t admin_state) +bool +connection_send(const connection_t * connection, msgbuf_t * msgbuf, bool queue) { - parcAssertNotNull(conn, "Parameter conn must be non-null"); - if (!conn->ops) - return; - if ((admin_state != CONNECTION_STATE_UP) && (admin_state != CONNECTION_STATE_DOWN)) - return; - ioOperations_SetAdminState(conn->ops, admin_state); + assert(connection); + + /* NULL message means flush */ + if (!msgbuf) + return _connection_send(connection, NULL, false); + + if (!connection_is_up(connection)) + return false; + + if (msgbuf_get_type(msgbuf) == MESSAGE_TYPE_DATA) { + uint8_t conn_id = (uint8_t)connection_get_id(connection); + msgbuf_update_pathlabel(msgbuf, conn_id); + } + + if (connection->wldr) + wldr_set_label(connection->wldr, msgbuf); + else + msgbuf_reset_wldr_label(msgbuf); + + return _connection_send(connection, msgbuf, queue); } -#ifdef WITH_POLICY -uint32_t connection_GetPriority(const Connection *conn) +/* + * here the wldr header is alreay set: this message is a retransmission or a + * notification + * + * we need to recompute the path label since we always store a pointer to + * the same message if this message will be sent again to someone else, the + * new path label must be computed starting from the orignal label. Note + * that we heve the same problem in case of PIT aggregation. That case is + * handled inside the MessageProcessor. This is specific to WLDR + * retransmittions. This is done only for data packets + */ +bool +connection_resend(const connection_t * connection, msgbuf_t * msgbuf, bool + notification) { - parcAssertNotNull(conn, "Parameter conn must be non-null"); - if (!conn->ops) - return 0; - return ioOperations_GetPriority(conn->ops); + assert(connection); + assert(msgbuf); + + bool ret = false; + + if (!connection_is_up(connection)) + return ret; + + if (msgbuf_get_type(msgbuf) == MESSAGE_TYPE_DATA) { + uint8_t conn_id = (uint8_t)connection_get_id(connection); + uint32_t old_path_label = msgbuf_get_pathlabel(msgbuf); + msgbuf_update_pathlabel(msgbuf, conn_id); + ret = _connection_send(connection, msgbuf, false); /* no queueing */ + msgbuf_set_pathlabel(msgbuf, old_path_label); + } else { + ret = _connection_send(connection, msgbuf, false); /* no queueing */ + } + + return ret; } -void connection_SetPriority(Connection *conn, uint32_t priority) -{ +#if 0 +bool connection_sendv(const connection_t * conn, struct iovec *msg, + size_t size) { parcAssertNotNull(conn, "Parameter conn must be non-null"); - if (!conn->ops) - return; - ioOperations_SetPriority(conn->ops, priority); + parcAssertNotNull(msg, "Parameter message must be non-null"); + + return ioOperations_SendIOVBuffer(conn->ops, msg, size); } -#endif /* WITH_POLICY */ -const char * connection_GetInterfaceName(const Connection * conn) -{ +void connection_probe(connection_t * conn, uint8_t * probe) { + ioOperations_SendProbe(conn->ops, probe); +} + +void connection_hangle_probe(connection_t * conn, uint8_t *probe){ parcAssertNotNull(conn, "Parameter conn must be non-null"); - if (!conn->ops) - return NULL; - return ioOperations_GetInterfaceName(conn->ops); + parcAssertNotNull(probe, "Parameter pkt must be non-null"); + + if(messageHandler_IsInterest(probe)){ + messageHandler_CreateProbeReply(probe, HF_INET6_TCP); + ioOperations_SendProbe(conn->ops, probe); + } } +#endif -#ifdef WITH_POLICY -void connection_AddTag(Connection *conn, policy_tag_t tag) +/* WLDR */ + +void +connection_wldr_allow_autostart(connection_t * connection, bool value) { - policy_tags_add(&conn->tags, tag); + connection->wldr_autostart = value; } -void connection_RemoveTag(Connection *conn, policy_tag_t tag) +bool +connection_wldr_autostart_is_allowed(connection_t * connection) { - policy_tags_remove(&conn->tags, tag); + return connection->wldr_autostart; } -policy_tags_t connection_GetTags(const Connection *conn) +void +connection_wldr_enable(connection_t * connection, bool value) { - return conn->tags; + if (connection_is_local(connection)) + return; + if (value) { + if (connection->wldr) + return; + connection->wldr = wldr_create(); + } else { + if (!connection->wldr) + return; + wldr_free(connection->wldr); + } } -void connection_SetTags(Connection *conn, policy_tags_t tags) +bool +connection_has_wldr(const connection_t * connection) { - conn->tags = tags; + return !!connection->wldr; } -void connection_ClearTags(Connection *conn) +void +connection_wldr_detect_losses(const connection_t * connection, msgbuf_t * msgbuf) { - conn->tags = POLICY_TAGS_EMPTY; + if (!connection->wldr) + return; + wldr_detect_losses(connection->wldr, connection, msgbuf); } -int connection_HasTag(const Connection *conn, policy_tag_t tag) +void +connection_wldr_handle_notification(const connection_t * connection, msgbuf_t * msgbuf) { - return policy_tags_has(conn->tags, tag); + if (!connection->wldr) + return; + wldr_handle_notification(connection->wldr, connection, msgbuf); } - -#endif /* WITH_POLICY */ |