[core,gateway] pass rdpContext

* pass rdpContext to freerdp_tls_new
* check freerdp_shall_disconnect_context in http_response_recv
This commit is contained in:
akallabeth 2024-06-27 08:40:05 +02:00
parent 559e770982
commit 1de8f5a7aa
No known key found for this signature in database
GPG Key ID: A49454A3FC909FD5
7 changed files with 85 additions and 91 deletions

View File

@ -1220,11 +1220,13 @@ int http_chuncked_read(BIO* bio, BYTE* pBuffer, size_t size,
}
}
#define sleep_or_timeout(startMS, timeoutMS) \
sleep_or_timeout_((startMS), (timeoutMS), __FILE__, __func__, __LINE__)
static BOOL sleep_or_timeout_(UINT64 startMS, UINT32 timeoutMS, const char* file, const char* fkt,
size_t line)
#define sleep_or_timeout(tls, startMS, timeoutMS) \
sleep_or_timeout_((tls), (startMS), (timeoutMS), __FILE__, __func__, __LINE__)
static BOOL sleep_or_timeout_(rdpTls* tls, UINT64 startMS, UINT32 timeoutMS, const char* file,
const char* fkt, size_t line)
{
WINPR_ASSERT(tls);
USleep(100);
const UINT64 nowMS = GetTickCount64();
if (nowMS - startMS > timeoutMS)
@ -1236,6 +1238,15 @@ static BOOL sleep_or_timeout_(UINT64 startMS, UINT32 timeoutMS, const char* file
"timeout [%" PRIu32 "ms] exceeded", timeoutMS);
return TRUE;
}
if (!BIO_should_retry(tls->bio))
{
WLog_ERR(TAG, "Retries exceeded");
ERR_print_errors_cb(print_bio_error, NULL);
return TRUE;
}
if (freerdp_shall_disconnect_context(tls->context))
return TRUE;
return FALSE;
}
@ -1245,7 +1256,8 @@ static SSIZE_T http_response_recv_line(rdpTls* tls, HttpResponse* response)
WINPR_ASSERT(response);
SSIZE_T payloadOffset = -1;
const UINT32 timeoutMS = freerdp_settings_get_uint32(tls->settings, FreeRDP_TcpConnectTimeout);
const UINT32 timeoutMS =
freerdp_settings_get_uint32(tls->context->settings, FreeRDP_TcpConnectTimeout);
const UINT64 startMS = GetTickCount64();
while (payloadOffset <= 0)
{
@ -1260,14 +1272,7 @@ static SSIZE_T http_response_recv_line(rdpTls* tls, HttpResponse* response)
status = BIO_read(tls->bio, Stream_Pointer(response->data), 1);
if (status <= 0)
{
if (!BIO_should_retry(tls->bio))
{
WLog_ERR(TAG, "Retries exceeded");
ERR_print_errors_cb(print_bio_error, NULL);
goto out_error;
}
if (sleep_or_timeout(startMS, timeoutMS))
if (sleep_or_timeout(tls, startMS, timeoutMS))
goto out_error;
continue;
}
@ -1312,7 +1317,8 @@ static BOOL http_response_recv_body(rdpTls* tls, HttpResponse* response, BOOL re
WINPR_ASSERT(response);
const UINT64 startMS = GetTickCount64();
const UINT32 timeoutMS = freerdp_settings_get_uint32(tls->settings, FreeRDP_TcpConnectTimeout);
const UINT32 timeoutMS =
freerdp_settings_get_uint32(tls->context->settings, FreeRDP_TcpConnectTimeout);
if ((response->TransferEncoding == TransferEncodingChunked) && readContentLength)
{
@ -1330,14 +1336,7 @@ static BOOL http_response_recv_body(rdpTls* tls, HttpResponse* response, BOOL re
Stream_GetRemainingCapacity(response->data), &ctx);
if (status <= 0)
{
if (!BIO_should_retry(tls->bio))
{
WLog_ERR(TAG, "Retries exceeded");
ERR_print_errors_cb(print_bio_error, NULL);
goto out_error;
}
if (sleep_or_timeout(startMS, timeoutMS))
if (sleep_or_timeout(tls, startMS, timeoutMS))
goto out_error;
}
else
@ -1365,14 +1364,7 @@ static BOOL http_response_recv_body(rdpTls* tls, HttpResponse* response, BOOL re
if (status <= 0)
{
if (!BIO_should_retry(tls->bio))
{
WLog_ERR(TAG, "Retries exceeded");
ERR_print_errors_cb(print_bio_error, NULL);
goto out_error;
}
if (sleep_or_timeout(startMS, timeoutMS))
if (sleep_or_timeout(tls, startMS, timeoutMS))
goto out_error;
continue;
}

View File

@ -117,7 +117,6 @@ typedef struct
struct rdp_rdg
{
rdpContext* context;
rdpSettings* settings;
BOOL attached;
BIO* frontBio;
rdpTls* tlsIn;
@ -500,7 +499,8 @@ static BOOL rdg_send_tunnel_request(rdpRdg* rdg)
if (rdg->extAuth == HTTP_EXTENDED_AUTH_PAA)
{
PAACookie = ConvertUtf8ToWCharAlloc(rdg->settings->GatewayAccessToken, &PAACookieLen);
PAACookie =
ConvertUtf8ToWCharAlloc(rdg->context->settings->GatewayAccessToken, &PAACookieLen);
if (!PAACookie || (PAACookieLen > UINT16_MAX / sizeof(WCHAR)))
{
@ -553,8 +553,8 @@ static BOOL rdg_send_tunnel_authorization(rdpRdg* rdg)
BOOL status = 0;
WINPR_ASSERT(rdg);
size_t clientNameLen = 0;
WCHAR* clientName =
freerdp_settings_get_string_as_utf16(rdg->settings, FreeRDP_ClientHostname, &clientNameLen);
WCHAR* clientName = freerdp_settings_get_string_as_utf16(
rdg->context->settings, FreeRDP_ClientHostname, &clientNameLen);
if (!clientName || (clientNameLen >= UINT16_MAX / sizeof(WCHAR)))
{
@ -600,8 +600,8 @@ static BOOL rdg_send_channel_create(rdpRdg* rdg)
size_t serverNameLen = 0;
WINPR_ASSERT(rdg);
serverName =
freerdp_settings_get_string_as_utf16(rdg->settings, FreeRDP_ServerHostname, &serverNameLen);
serverName = freerdp_settings_get_string_as_utf16(rdg->context->settings,
FreeRDP_ServerHostname, &serverNameLen);
if (!serverName || (serverNameLen >= UINT16_MAX / sizeof(WCHAR)))
goto fail;
@ -618,7 +618,8 @@ static BOOL rdg_send_channel_create(rdpRdg* rdg)
Stream_Write_UINT32(s, packetSize); /* PacketLength (4 bytes) */
Stream_Write_UINT8(s, 1); /* Number of resources. (1 byte) */
Stream_Write_UINT8(s, 0); /* Number of alternative resources (1 byte) */
Stream_Write_UINT16(s, (UINT16)rdg->settings->ServerPort); /* Resource port (2 bytes) */
Stream_Write_UINT16(s,
(UINT16)rdg->context->settings->ServerPort); /* Resource port (2 bytes) */
Stream_Write_UINT16(s, 3); /* Protocol number (2 bytes) */
Stream_Write_UINT16(s, (UINT16)serverNameLen * sizeof(WCHAR));
Stream_Write_UTF16_String(s, serverName, (size_t)serverNameLen);
@ -688,7 +689,7 @@ static wStream* rdg_build_http_request(rdpRdg* rdg, const char* method,
else if (rdg->extAuth == HTTP_EXTENDED_AUTH_BEARER)
{
http_request_set_auth_scheme(request, "Bearer");
http_request_set_auth_param(request, rdg->settings->GatewayHttpExtAuthBearer);
http_request_set_auth_param(request, rdg->context->settings->GatewayHttpExtAuthBearer);
}
http_request_set_transfer_encoding(request, transferEncoding);
@ -1269,7 +1270,7 @@ static BOOL rdg_tls_connect(rdpRdg* rdg, rdpTls* tls, const char* peerAddress, i
long status = 0;
BIO* socketBio = NULL;
BIO* bufferedBio = NULL;
rdpSettings* settings = rdg->settings;
rdpSettings* settings = rdg->context->settings;
const char* peerHostname = settings->GatewayHostname;
UINT16 peerPort = (UINT16)settings->GatewayPort;
const char* proxyUsername = NULL;
@ -1355,7 +1356,7 @@ static BOOL rdg_establish_data_connection(rdpRdg* rdg, rdpTls* tls, const char*
return FALSE;
WINPR_ASSERT(rpcFallback);
if (rdg->settings->GatewayHttpExtAuthBearer && rdg->extAuth == HTTP_EXTENDED_AUTH_NONE)
if (rdg->context->settings->GatewayHttpExtAuthBearer && rdg->extAuth == HTTP_EXTENDED_AUTH_NONE)
rdg->extAuth = HTTP_EXTENDED_AUTH_BEARER;
if (rdg->extAuth == HTTP_EXTENDED_AUTH_NONE)
{
@ -2216,21 +2217,22 @@ rdpRdg* rdg_new(rdpContext* context)
rdg->log = WLog_Get(TAG);
rdg->state = RDG_CLIENT_STATE_INITIAL;
rdg->context = context;
rdg->settings = rdg->context->settings;
rdg->extAuth = (rdg->settings->GatewayHttpExtAuthSspiNtlm ? HTTP_EXTENDED_AUTH_SSPI_NTLM
: HTTP_EXTENDED_AUTH_NONE);
rdg->context->settings = rdg->context->settings;
rdg->extAuth =
(rdg->context->settings->GatewayHttpExtAuthSspiNtlm ? HTTP_EXTENDED_AUTH_SSPI_NTLM
: HTTP_EXTENDED_AUTH_NONE);
if (rdg->settings->GatewayAccessToken)
if (rdg->context->settings->GatewayAccessToken)
rdg->extAuth = HTTP_EXTENDED_AUTH_PAA;
UuidCreate(&rdg->guid);
rdg->tlsOut = freerdp_tls_new(rdg->settings);
rdg->tlsOut = freerdp_tls_new(rdg->context);
if (!rdg->tlsOut)
goto rdg_alloc_error;
rdg->tlsIn = freerdp_tls_new(rdg->settings);
rdg->tlsIn = freerdp_tls_new(rdg->context);
if (!rdg->tlsIn)
goto rdg_alloc_error;
@ -2246,12 +2248,12 @@ rdpRdg* rdg_new(rdpContext* context)
!http_context_set_pragma(rdg->http, "no-cache") ||
!http_context_set_connection(rdg->http, "Keep-Alive") ||
!http_context_set_user_agent(rdg->http, "MS-RDGateway/1.0") ||
!http_context_set_host(rdg->http, rdg->settings->GatewayHostname) ||
!http_context_set_host(rdg->http, rdg->context->settings->GatewayHostname) ||
!http_context_set_rdg_connection_id(rdg->http, &rdg->guid) ||
!http_context_set_rdg_correlation_id(rdg->http, &rdg->guid) ||
!http_context_enable_websocket_upgrade(
rdg->http,
freerdp_settings_get_bool(rdg->settings, FreeRDP_GatewayHttpUseWebsockets)))
rdg->http, freerdp_settings_get_bool(rdg->context->settings,
FreeRDP_GatewayHttpUseWebsockets)))
{
goto rdg_alloc_error;
}

View File

@ -762,7 +762,7 @@ static BOOL rpc_channel_tls_connect(RpcChannel* channel, UINT32 timeout)
}
channel->bio = bufferedBio;
tls = channel->tls = freerdp_tls_new(settings);
tls = channel->tls = freerdp_tls_new(context);
if (!tls)
return FALSE;

View File

@ -53,7 +53,6 @@
struct rdp_wst
{
rdpContext* context;
rdpSettings* settings;
BOOL attached;
BIO* frontBio;
rdpTls* tls;
@ -217,7 +216,7 @@ static BOOL wst_tls_connect(rdpWst* wst, rdpTls* tls, int timeout)
long status = 0;
BIO* socketBio = NULL;
BIO* bufferedBio = NULL;
rdpSettings* settings = wst->settings;
rdpSettings* settings = wst->context->settings;
const char* peerHostname = wst->gwhostname;
UINT16 peerPort = wst->gwport;
const char* proxyUsername = NULL;
@ -313,11 +312,12 @@ static wStream* wst_build_http_request(rdpWst* wst)
if (!wst_set_auth_header(wst->auth, request))
goto out;
}
else if (freerdp_settings_get_string(wst->settings, FreeRDP_GatewayHttpExtAuthBearer))
else if (freerdp_settings_get_string(wst->context->settings, FreeRDP_GatewayHttpExtAuthBearer))
{
http_request_set_auth_scheme(request, "Bearer");
http_request_set_auth_param(
request, freerdp_settings_get_string(wst->settings, FreeRDP_GatewayHttpExtAuthBearer));
request,
freerdp_settings_get_string(wst->context->settings, FreeRDP_GatewayHttpExtAuthBearer));
}
s = http_request_write(wst->http, request);
@ -362,7 +362,7 @@ static BOOL wst_handle_ok_or_forbidden(rdpWst* wst, HttpResponse** ppresponse, D
/* AVD returns a 403 response with a ARRAffinity cookie set. retry with that cookie */
const char* affinity = http_response_get_setcookie(*ppresponse, "ARRAffinity");
if (affinity && freerdp_settings_get_bool(wst->settings, FreeRDP_GatewayArmTransport))
if (affinity && freerdp_settings_get_bool(wst->context->settings, FreeRDP_GatewayArmTransport))
{
WLog_DBG(TAG, "Got Affinity cookie %s", affinity);
http_context_set_cookie(wst->http, "ARRAffinity", affinity);
@ -374,19 +374,19 @@ static BOOL wst_handle_ok_or_forbidden(rdpWst* wst, HttpResponse** ppresponse, D
closesocket((SOCKET)fd);
freerdp_tls_free(wst->tls);
wst->tls = freerdp_tls_new(wst->settings);
wst->tls = freerdp_tls_new(wst->context);
if (!wst_tls_connect(wst, wst->tls, timeout))
return FALSE;
if (freerdp_settings_get_string(wst->settings, FreeRDP_GatewayHttpExtAuthBearer) &&
freerdp_settings_get_bool(wst->settings, FreeRDP_GatewayArmTransport))
if (freerdp_settings_get_string(wst->context->settings, FreeRDP_GatewayHttpExtAuthBearer) &&
freerdp_settings_get_bool(wst->context->settings, FreeRDP_GatewayArmTransport))
{
char* urlWithAuth = NULL;
size_t urlLen = 0;
char firstParam = (strchr(wst->gwpath, '?') != NULL) ? '&' : '?';
winpr_asprintf(
&urlWithAuth, &urlLen, arm_query_param, wst->gwpath, firstParam,
freerdp_settings_get_string(wst->settings, FreeRDP_GatewayHttpExtAuthBearer));
winpr_asprintf(&urlWithAuth, &urlLen, arm_query_param, wst->gwpath, firstParam,
freerdp_settings_get_string(wst->context->settings,
FreeRDP_GatewayHttpExtAuthBearer));
if (!urlWithAuth)
return FALSE;
free(wst->gwpath);
@ -416,7 +416,7 @@ static BOOL wst_handle_denied(rdpWst* wst, HttpResponse** ppresponse, long* pSta
WINPR_ASSERT(*ppresponse);
WINPR_ASSERT(pStatusCode);
if (freerdp_settings_get_string(wst->settings, FreeRDP_GatewayHttpExtAuthBearer))
if (freerdp_settings_get_string(wst->context->settings, FreeRDP_GatewayHttpExtAuthBearer))
return FALSE;
if (!wst_auth_init(wst, wst->tls, AUTH_PKG))
@ -457,7 +457,7 @@ BOOL wst_connect(rdpWst* wst, DWORD timeout)
WINPR_ASSERT(wst);
if (!wst_tls_connect(wst, wst->tls, timeout))
return FALSE;
if (freerdp_settings_get_bool(wst->settings, FreeRDP_GatewayArmTransport))
if (freerdp_settings_get_bool(wst->context->settings, FreeRDP_GatewayArmTransport))
{
/*
* If we are directed here from a ARM Gateway first
@ -793,7 +793,6 @@ rdpWst* wst_new(rdpContext* context)
if (wst)
{
wst->context = context;
wst->settings = wst->context->settings;
wst->gwhostname = NULL;
wst->gwport = 443;
@ -802,7 +801,7 @@ rdpWst* wst_new(rdpContext* context)
if (!wst_parse_url(wst, context->settings->GatewayUrl))
goto wst_alloc_error;
wst->tls = freerdp_tls_new(wst->settings);
wst->tls = freerdp_tls_new(wst->context);
if (!wst->tls)
goto wst_alloc_error;

View File

@ -291,7 +291,7 @@ static BOOL transport_default_connect_tls(rdpTransport* transport)
settings = context->settings;
WINPR_ASSERT(settings);
if (!(tls = freerdp_tls_new(settings)))
if (!(tls = freerdp_tls_new(context)))
return FALSE;
transport->tls = tls;
@ -629,7 +629,7 @@ static BOOL transport_default_accept_tls(rdpTransport* transport)
WINPR_ASSERT(settings);
if (!transport->tls)
transport->tls = freerdp_tls_new(settings);
transport->tls = freerdp_tls_new(context);
transport->layer = TRANSPORT_LAYER_TLS;

View File

@ -750,7 +750,7 @@ static BOOL tls_prepare(rdpTls* tls, BIO* underlying, SSL_METHOD* method, int op
{
WINPR_ASSERT(tls);
rdpSettings* settings = tls->settings;
rdpSettings* settings = tls->context->settings;
WINPR_ASSERT(settings);
tls_reset(tls);
@ -1295,7 +1295,7 @@ static BOOL tls_match_hostname(const char* pattern, const size_t pattern_length,
static BOOL is_redirected(rdpTls* tls)
{
rdpSettings* settings = tls->settings;
rdpSettings* settings = tls->context->settings;
if (LB_NOREDIRECT & settings->RedirectionFlags)
return FALSE;
@ -1305,7 +1305,7 @@ static BOOL is_redirected(rdpTls* tls)
static BOOL is_accepted(rdpTls* tls, const BYTE* pem, size_t length)
{
rdpSettings* settings = tls->settings;
rdpSettings* settings = tls->context->settings;
char* AccpetedKey = NULL;
UINT32 AcceptedKeyLength = 0;
@ -1431,7 +1431,7 @@ static BOOL accept_cert(rdpTls* tls, const BYTE* pem, UINT32 length)
FreeRDP_Settings_Keys_String id = FreeRDP_AcceptedCert;
FreeRDP_Settings_Keys_UInt32 lid = FreeRDP_AcceptedCertLength;
rdpSettings* settings = tls->settings;
rdpSettings* settings = tls->context->settings;
if (tls->isGatewayTransport)
{
@ -1477,9 +1477,9 @@ int tls_verify_certificate(rdpTls* tls, const rdpCertificate* cert, const char*
freerdp* instance = NULL;
WINPR_ASSERT(tls);
WINPR_ASSERT(tls->settings);
WINPR_ASSERT(tls->context->settings);
instance = (freerdp*)tls->settings->instance;
instance = (freerdp*)tls->context->settings->instance;
WINPR_ASSERT(instance);
if (freerdp_shall_disconnect_context(instance->context))
@ -1495,7 +1495,7 @@ int tls_verify_certificate(rdpTls* tls, const rdpCertificate* cert, const char*
goto end;
}
if (is_accepted_fingerprint(cert, tls->settings->CertificateAcceptedFingerprints))
if (is_accepted_fingerprint(cert, tls->context->settings->CertificateAcceptedFingerprints))
{
verification_status = 1;
goto end;
@ -1511,7 +1511,7 @@ int tls_verify_certificate(rdpTls* tls, const rdpCertificate* cert, const char*
flags |= VERIFY_CERT_FLAG_REDIRECT;
/* Certificate management is done by the application */
if (tls->settings->ExternalCertificateManagement)
if (tls->context->settings->ExternalCertificateManagement)
{
if (instance->VerifyX509Certificate)
verification_status =
@ -1529,15 +1529,15 @@ int tls_verify_certificate(rdpTls* tls, const rdpCertificate* cert, const char*
}
}
/* ignore certificate verification if user explicitly required it (discouraged) */
else if (tls->settings->IgnoreCertificate)
else if (tls->context->settings->IgnoreCertificate)
verification_status = 1; /* success! */
else if (!tls->isGatewayTransport && (tls->settings->AuthenticationLevel == 0))
else if (!tls->isGatewayTransport && (tls->context->settings->AuthenticationLevel == 0))
verification_status = 1; /* success! */
else
{
/* if user explicitly specified a certificate name, use it instead of the hostname */
if (!tls->isGatewayTransport && tls->settings->CertificateName)
hostname = tls->settings->CertificateName;
if (!tls->isGatewayTransport && tls->context->settings->CertificateName)
hostname = tls->context->settings->CertificateName;
/* attempt verification using OpenSSL and the ~/.freerdp/certs certificate store */
certificate_status = freerdp_certificate_verify(
@ -1612,12 +1612,12 @@ int tls_verify_certificate(rdpTls* tls, const rdpCertificate* cert, const char*
}
/* Automatically accept certificate on first use */
if (tls->settings->AutoAcceptCertificate)
if (tls->context->settings->AutoAcceptCertificate)
{
WLog_INFO(TAG, "No certificate stored, automatically accepting.");
accept_certificate = 1;
}
else if (tls->settings->AutoDenyCertificate)
else if (tls->context->settings->AutoDenyCertificate)
{
WLog_INFO(TAG, "No certificate stored, automatically denying.");
accept_certificate = 0;
@ -1637,7 +1637,7 @@ int tls_verify_certificate(rdpTls* tls, const rdpCertificate* cert, const char*
else if (instance->VerifyCertificateEx)
{
const BOOL use_pem = freerdp_settings_get_bool(
tls->settings, FreeRDP_CertificateCallbackPreferPEM);
tls->context->settings, FreeRDP_CertificateCallbackPreferPEM);
char* fp = NULL;
DWORD cflags = flags;
if (use_pem)
@ -1682,7 +1682,7 @@ int tls_verify_certificate(rdpTls* tls, const rdpCertificate* cert, const char*
WLog_WARN(TAG, "Failed to get certificate entry for %s:%" PRIu16 "", hostname,
port);
if (tls->settings->AutoDenyCertificate)
if (tls->context->settings->AutoDenyCertificate)
{
WLog_INFO(TAG, "No certificate stored, automatically denying.");
accept_certificate = 0;
@ -1708,8 +1708,9 @@ int tls_verify_certificate(rdpTls* tls, const rdpCertificate* cert, const char*
const char* old_fp = freerdp_certificate_data_get_fingerprint(stored_data);
const char* old_pem = freerdp_certificate_data_get_pem(stored_data);
const BOOL fpIsAllocated =
!old_pem || !freerdp_settings_get_bool(
tls->settings, FreeRDP_CertificateCallbackPreferPEM);
!old_pem ||
!freerdp_settings_get_bool(tls->context->settings,
FreeRDP_CertificateCallbackPreferPEM);
char* fp = NULL;
if (!fpIsAllocated)
{
@ -1857,7 +1858,7 @@ void tls_print_certificate_name_mismatch_error(const char* hostname, UINT16 port
WLog_ERR(TAG, "A valid certificate for the wrong name should NOT be trusted!");
}
rdpTls* freerdp_tls_new(rdpSettings* settings)
rdpTls* freerdp_tls_new(rdpContext* context)
{
rdpTls* tls = NULL;
tls = (rdpTls*)calloc(1, sizeof(rdpTls));
@ -1865,11 +1866,11 @@ rdpTls* freerdp_tls_new(rdpSettings* settings)
if (!tls)
return NULL;
tls->settings = settings;
tls->context = context;
if (!settings->ServerMode)
if (!freerdp_settings_get_bool(tls->context->settings, FreeRDP_ServerMode))
{
tls->certificate_store = freerdp_certificate_store_new(settings);
tls->certificate_store = freerdp_certificate_store_new(tls->context->settings);
if (!tls->certificate_store)
goto out_free;

View File

@ -71,7 +71,7 @@ struct rdp_tls
SSL_CTX* ctx;
BYTE* PublicKey;
DWORD PublicKeyLength;
rdpSettings* settings;
rdpContext* context;
SecPkgContext_Bindings* Bindings;
rdpCertificateStore* certificate_store;
BIO* underlying;
@ -122,7 +122,7 @@ extern "C"
FREERDP_LOCAL void freerdp_tls_free(rdpTls* tls);
WINPR_ATTR_MALLOC(freerdp_tls_free, 1)
FREERDP_LOCAL rdpTls* freerdp_tls_new(rdpSettings* settings);
FREERDP_LOCAL rdpTls* freerdp_tls_new(rdpContext* context);
#ifdef __cplusplus
}