2013-03-30 01:53:18 +04:00
|
|
|
/**
|
|
|
|
* FreeRDP: A Remote Desktop Protocol Implementation
|
|
|
|
* Channel Addins
|
|
|
|
*
|
|
|
|
* Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com>
|
2015-06-02 10:56:10 +03:00
|
|
|
* Copyright 2015 Thincast Technologies GmbH
|
|
|
|
* Copyright 2015 DI (FH) Martin Haimberger <martin.haimberger@thincast.com>
|
2013-03-30 01:53:18 +04:00
|
|
|
*
|
|
|
|
* 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
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
|
2022-02-16 13:20:38 +03:00
|
|
|
#include <freerdp/config.h>
|
2013-03-30 01:53:18 +04:00
|
|
|
|
|
|
|
#include <winpr/crt.h>
|
2021-12-22 11:04:04 +03:00
|
|
|
#include <winpr/assert.h>
|
2013-03-30 01:53:18 +04:00
|
|
|
#include <winpr/path.h>
|
2021-04-12 12:06:45 +03:00
|
|
|
#include <winpr/string.h>
|
2013-03-30 01:53:18 +04:00
|
|
|
#include <winpr/file.h>
|
|
|
|
#include <winpr/synch.h>
|
|
|
|
#include <winpr/library.h>
|
|
|
|
#include <winpr/collections.h>
|
|
|
|
|
2023-03-14 12:39:18 +03:00
|
|
|
#include <freerdp/freerdp.h>
|
2013-04-02 02:21:21 +04:00
|
|
|
#include <freerdp/addin.h>
|
2015-11-05 16:02:07 +03:00
|
|
|
#include <freerdp/build-config.h>
|
2013-04-02 02:21:21 +04:00
|
|
|
#include <freerdp/client/channels.h>
|
|
|
|
|
2013-03-30 01:53:18 +04:00
|
|
|
#include "tables.h"
|
|
|
|
|
|
|
|
#include "addin.h"
|
|
|
|
|
2015-06-02 10:56:10 +03:00
|
|
|
#include <freerdp/channels/log.h>
|
|
|
|
#define TAG CHANNELS_TAG("addin")
|
|
|
|
|
2013-03-30 01:53:18 +04:00
|
|
|
extern const STATIC_ENTRY_TABLE CLIENT_STATIC_ENTRY_TABLES[];
|
|
|
|
|
2019-11-20 13:30:14 +03:00
|
|
|
static void* freerdp_channels_find_static_entry_in_table(const STATIC_ENTRY_TABLE* table,
|
|
|
|
const char* identifier)
|
2013-03-30 01:53:18 +04:00
|
|
|
{
|
2019-11-12 13:04:11 +03:00
|
|
|
size_t index = 0;
|
2021-08-02 13:13:34 +03:00
|
|
|
const STATIC_ENTRY* pEntry = (const STATIC_ENTRY*)&table->table[index++];
|
2013-03-30 01:53:18 +04:00
|
|
|
|
|
|
|
while (pEntry->entry != NULL)
|
|
|
|
{
|
|
|
|
if (strcmp(pEntry->name, identifier) == 0)
|
|
|
|
{
|
2019-11-06 17:24:51 +03:00
|
|
|
return (void*)pEntry->entry;
|
2013-03-30 01:53:18 +04:00
|
|
|
}
|
|
|
|
|
2021-08-02 13:13:34 +03:00
|
|
|
pEntry = (const STATIC_ENTRY*)&table->table[index++];
|
2013-03-30 01:53:18 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
void* freerdp_channels_client_find_static_entry(const char* name, const char* identifier)
|
|
|
|
{
|
2019-11-12 13:04:11 +03:00
|
|
|
size_t index = 0;
|
2021-06-18 12:15:28 +03:00
|
|
|
const STATIC_ENTRY_TABLE* pEntry = &CLIENT_STATIC_ENTRY_TABLES[index++];
|
2013-03-30 01:53:18 +04:00
|
|
|
|
|
|
|
while (pEntry->table != NULL)
|
|
|
|
{
|
|
|
|
if (strcmp(pEntry->name, name) == 0)
|
|
|
|
{
|
|
|
|
return freerdp_channels_find_static_entry_in_table(pEntry, identifier);
|
|
|
|
}
|
|
|
|
|
2021-06-18 12:15:28 +03:00
|
|
|
pEntry = &CLIENT_STATIC_ENTRY_TABLES[index++];
|
2013-03-30 01:53:18 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
extern const STATIC_ADDIN_TABLE CLIENT_STATIC_ADDIN_TABLE[];
|
|
|
|
|
2019-11-06 17:24:51 +03:00
|
|
|
static FREERDP_ADDIN** freerdp_channels_list_client_static_addins(LPCSTR pszName,
|
|
|
|
LPCSTR pszSubsystem,
|
|
|
|
LPCSTR pszType, DWORD dwFlags)
|
2013-03-30 01:53:18 +04:00
|
|
|
{
|
2024-01-23 18:49:54 +03:00
|
|
|
DWORD nAddins = 0;
|
2013-03-30 01:53:18 +04:00
|
|
|
FREERDP_ADDIN** ppAddins = NULL;
|
2024-01-23 18:49:54 +03:00
|
|
|
const STATIC_SUBSYSTEM_ENTRY* subsystems = NULL;
|
2013-03-30 01:53:18 +04:00
|
|
|
nAddins = 0;
|
2019-11-06 17:24:51 +03:00
|
|
|
ppAddins = (FREERDP_ADDIN**)calloc(128, sizeof(FREERDP_ADDIN*));
|
2017-02-20 20:31:58 +03:00
|
|
|
|
2015-06-02 10:56:10 +03:00
|
|
|
if (!ppAddins)
|
|
|
|
{
|
|
|
|
WLog_ERR(TAG, "calloc failed!");
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
2013-03-30 01:53:18 +04:00
|
|
|
ppAddins[nAddins] = NULL;
|
|
|
|
|
2024-01-30 12:25:38 +03:00
|
|
|
for (size_t i = 0; CLIENT_STATIC_ADDIN_TABLE[i].name != NULL; i++)
|
2013-03-30 01:53:18 +04:00
|
|
|
{
|
2019-11-06 17:24:51 +03:00
|
|
|
FREERDP_ADDIN* pAddin = (FREERDP_ADDIN*)calloc(1, sizeof(FREERDP_ADDIN));
|
2017-02-20 20:31:58 +03:00
|
|
|
|
2015-06-02 10:56:10 +03:00
|
|
|
if (!pAddin)
|
|
|
|
{
|
|
|
|
WLog_ERR(TAG, "calloc failed!");
|
|
|
|
goto error_out;
|
|
|
|
}
|
2013-03-30 01:53:18 +04:00
|
|
|
|
2018-08-24 10:54:25 +03:00
|
|
|
sprintf_s(pAddin->cName, ARRAYSIZE(pAddin->cName), "%s", CLIENT_STATIC_ADDIN_TABLE[i].name);
|
2013-03-30 01:53:18 +04:00
|
|
|
pAddin->dwFlags = FREERDP_ADDIN_CLIENT;
|
|
|
|
pAddin->dwFlags |= FREERDP_ADDIN_STATIC;
|
|
|
|
pAddin->dwFlags |= FREERDP_ADDIN_NAME;
|
|
|
|
ppAddins[nAddins++] = pAddin;
|
2021-08-02 13:13:34 +03:00
|
|
|
subsystems = (const STATIC_SUBSYSTEM_ENTRY*)CLIENT_STATIC_ADDIN_TABLE[i].table;
|
2013-03-30 01:53:18 +04:00
|
|
|
|
2024-01-30 12:25:38 +03:00
|
|
|
for (size_t j = 0; subsystems[j].name != NULL; j++)
|
2013-03-30 01:53:18 +04:00
|
|
|
{
|
2019-11-06 17:24:51 +03:00
|
|
|
pAddin = (FREERDP_ADDIN*)calloc(1, sizeof(FREERDP_ADDIN));
|
2017-02-20 20:31:58 +03:00
|
|
|
|
2015-06-02 10:56:10 +03:00
|
|
|
if (!pAddin)
|
|
|
|
{
|
|
|
|
WLog_ERR(TAG, "calloc failed!");
|
|
|
|
goto error_out;
|
|
|
|
}
|
2013-03-30 01:53:18 +04:00
|
|
|
|
2019-11-06 17:24:51 +03:00
|
|
|
sprintf_s(pAddin->cName, ARRAYSIZE(pAddin->cName), "%s",
|
|
|
|
CLIENT_STATIC_ADDIN_TABLE[i].name);
|
2018-08-27 11:52:22 +03:00
|
|
|
sprintf_s(pAddin->cSubsystem, ARRAYSIZE(pAddin->cSubsystem), "%s", subsystems[j].name);
|
2013-03-30 01:53:18 +04:00
|
|
|
pAddin->dwFlags = FREERDP_ADDIN_CLIENT;
|
|
|
|
pAddin->dwFlags |= FREERDP_ADDIN_STATIC;
|
|
|
|
pAddin->dwFlags |= FREERDP_ADDIN_NAME;
|
|
|
|
pAddin->dwFlags |= FREERDP_ADDIN_SUBSYSTEM;
|
|
|
|
ppAddins[nAddins++] = pAddin;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return ppAddins;
|
2015-06-02 10:56:10 +03:00
|
|
|
error_out:
|
|
|
|
freerdp_channels_addin_list_free(ppAddins);
|
|
|
|
return NULL;
|
2013-03-30 01:53:18 +04:00
|
|
|
}
|
|
|
|
|
2022-09-26 12:22:02 +03:00
|
|
|
static HANDLE FindFirstFileUTF8(LPCSTR pszSearchPath, WIN32_FIND_DATAW* FindData)
|
|
|
|
{
|
|
|
|
HANDLE hdl = INVALID_HANDLE_VALUE;
|
2022-10-28 09:09:27 +03:00
|
|
|
if (!pszSearchPath)
|
|
|
|
return hdl;
|
|
|
|
WCHAR* wpath = ConvertUtf8ToWCharAlloc(pszSearchPath, NULL);
|
|
|
|
if (!wpath)
|
2022-09-26 12:22:02 +03:00
|
|
|
return hdl;
|
|
|
|
|
|
|
|
hdl = FindFirstFileW(wpath, FindData);
|
|
|
|
free(wpath);
|
|
|
|
|
|
|
|
return hdl;
|
|
|
|
}
|
|
|
|
|
2019-02-07 16:23:14 +03:00
|
|
|
static FREERDP_ADDIN** freerdp_channels_list_dynamic_addins(LPCSTR pszName, LPCSTR pszSubsystem,
|
2019-11-06 17:24:51 +03:00
|
|
|
LPCSTR pszType, DWORD dwFlags)
|
2013-03-30 01:53:18 +04:00
|
|
|
{
|
2024-01-23 18:49:54 +03:00
|
|
|
int nDashes = 0;
|
|
|
|
HANDLE hFind = NULL;
|
|
|
|
DWORD nAddins = 0;
|
|
|
|
LPSTR pszPattern = NULL;
|
|
|
|
size_t cchPattern = 0;
|
2013-03-30 01:53:18 +04:00
|
|
|
LPCSTR pszAddinPath = FREERDP_ADDIN_PATH;
|
|
|
|
LPCSTR pszInstallPrefix = FREERDP_INSTALL_PREFIX;
|
2024-01-23 18:49:54 +03:00
|
|
|
LPCSTR pszExtension = NULL;
|
|
|
|
LPSTR pszSearchPath = NULL;
|
|
|
|
size_t cchSearchPath = 0;
|
|
|
|
size_t cchAddinPath = 0;
|
|
|
|
size_t cchInstallPrefix = 0;
|
|
|
|
FREERDP_ADDIN** ppAddins = NULL;
|
2022-09-26 12:22:02 +03:00
|
|
|
WIN32_FIND_DATAW FindData = { 0 };
|
2019-10-29 12:18:09 +03:00
|
|
|
cchAddinPath = strnlen(pszAddinPath, sizeof(FREERDP_ADDIN_PATH));
|
|
|
|
cchInstallPrefix = strnlen(pszInstallPrefix, sizeof(FREERDP_INSTALL_PREFIX));
|
2013-03-30 01:53:18 +04:00
|
|
|
pszExtension = PathGetSharedLibraryExtensionA(0);
|
2019-10-29 12:18:09 +03:00
|
|
|
cchPattern = 128 + strnlen(pszExtension, MAX_PATH) + 2;
|
2019-11-06 17:24:51 +03:00
|
|
|
pszPattern = (LPSTR)malloc(cchPattern + 1);
|
2017-02-20 20:31:58 +03:00
|
|
|
|
2015-06-02 10:56:10 +03:00
|
|
|
if (!pszPattern)
|
|
|
|
{
|
|
|
|
WLog_ERR(TAG, "malloc failed!");
|
|
|
|
return NULL;
|
|
|
|
}
|
2013-03-30 01:53:18 +04:00
|
|
|
|
|
|
|
if (pszName && pszSubsystem && pszType)
|
|
|
|
{
|
2019-11-06 17:24:51 +03:00
|
|
|
sprintf_s(pszPattern, cchPattern, FREERDP_SHARED_LIBRARY_PREFIX "%s-client-%s-%s.%s",
|
2017-02-20 20:31:58 +03:00
|
|
|
pszName, pszSubsystem, pszType, pszExtension);
|
2013-03-30 01:53:18 +04:00
|
|
|
}
|
|
|
|
else if (pszName && pszType)
|
|
|
|
{
|
2019-11-06 17:24:51 +03:00
|
|
|
sprintf_s(pszPattern, cchPattern, FREERDP_SHARED_LIBRARY_PREFIX "%s-client-?-%s.%s",
|
2017-02-20 20:31:58 +03:00
|
|
|
pszName, pszType, pszExtension);
|
2013-03-30 01:53:18 +04:00
|
|
|
}
|
|
|
|
else if (pszName)
|
|
|
|
{
|
2019-11-06 17:24:51 +03:00
|
|
|
sprintf_s(pszPattern, cchPattern, FREERDP_SHARED_LIBRARY_PREFIX "%s-client*.%s", pszName,
|
|
|
|
pszExtension);
|
2013-03-30 01:53:18 +04:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2019-11-06 17:24:51 +03:00
|
|
|
sprintf_s(pszPattern, cchPattern, FREERDP_SHARED_LIBRARY_PREFIX "?-client*.%s",
|
2017-02-20 20:31:58 +03:00
|
|
|
pszExtension);
|
2013-03-30 01:53:18 +04:00
|
|
|
}
|
|
|
|
|
2019-10-29 12:18:09 +03:00
|
|
|
cchPattern = strnlen(pszPattern, cchPattern);
|
2013-03-30 01:53:18 +04:00
|
|
|
cchSearchPath = cchInstallPrefix + cchAddinPath + cchPattern + 3;
|
2023-07-26 11:33:30 +03:00
|
|
|
pszSearchPath = (LPSTR)calloc(cchSearchPath + 1, sizeof(char));
|
2017-02-20 20:31:58 +03:00
|
|
|
|
2015-06-02 10:56:10 +03:00
|
|
|
if (!pszSearchPath)
|
|
|
|
{
|
|
|
|
WLog_ERR(TAG, "malloc failed!");
|
|
|
|
free(pszPattern);
|
|
|
|
return NULL;
|
|
|
|
}
|
2013-03-30 01:53:18 +04:00
|
|
|
|
|
|
|
CopyMemory(pszSearchPath, pszInstallPrefix, cchInstallPrefix);
|
|
|
|
pszSearchPath[cchInstallPrefix] = '\0';
|
2023-07-26 11:33:30 +03:00
|
|
|
const HRESULT hr1 = NativePathCchAppendA(pszSearchPath, cchSearchPath + 1, pszAddinPath);
|
|
|
|
const HRESULT hr2 = NativePathCchAppendA(pszSearchPath, cchSearchPath + 1, pszPattern);
|
2013-08-28 17:44:37 +04:00
|
|
|
free(pszPattern);
|
2022-09-26 12:22:02 +03:00
|
|
|
|
2023-07-26 11:33:30 +03:00
|
|
|
if (FAILED(hr1) || FAILED(hr2))
|
|
|
|
{
|
|
|
|
free(pszSearchPath);
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
2022-09-26 12:22:02 +03:00
|
|
|
hFind = FindFirstFileUTF8(pszSearchPath, &FindData);
|
|
|
|
|
2013-08-28 17:45:41 +04:00
|
|
|
free(pszSearchPath);
|
2013-03-30 01:53:18 +04:00
|
|
|
nAddins = 0;
|
2019-11-06 17:24:51 +03:00
|
|
|
ppAddins = (FREERDP_ADDIN**)calloc(128, sizeof(FREERDP_ADDIN*));
|
2017-02-20 20:31:58 +03:00
|
|
|
|
2015-06-02 10:56:10 +03:00
|
|
|
if (!ppAddins)
|
|
|
|
{
|
2018-08-17 13:41:34 +03:00
|
|
|
FindClose(hFind);
|
2015-06-02 10:56:10 +03:00
|
|
|
WLog_ERR(TAG, "calloc failed!");
|
|
|
|
return NULL;
|
|
|
|
}
|
2013-03-30 01:53:18 +04:00
|
|
|
|
|
|
|
if (hFind == INVALID_HANDLE_VALUE)
|
|
|
|
return ppAddins;
|
|
|
|
|
|
|
|
do
|
|
|
|
{
|
2022-09-26 12:22:02 +03:00
|
|
|
char* cFileName = NULL;
|
2021-12-22 11:04:04 +03:00
|
|
|
BOOL used = FALSE;
|
|
|
|
FREERDP_ADDIN* pAddin = (FREERDP_ADDIN*)calloc(1, sizeof(FREERDP_ADDIN));
|
2017-02-20 20:31:58 +03:00
|
|
|
|
2015-06-02 10:56:10 +03:00
|
|
|
if (!pAddin)
|
|
|
|
{
|
|
|
|
WLog_ERR(TAG, "calloc failed!");
|
|
|
|
goto error_out;
|
|
|
|
}
|
2013-03-30 01:53:18 +04:00
|
|
|
|
2022-10-28 09:09:27 +03:00
|
|
|
cFileName =
|
|
|
|
ConvertWCharNToUtf8Alloc(FindData.cFileName, ARRAYSIZE(FindData.cFileName), NULL);
|
|
|
|
if (!cFileName)
|
2022-09-26 12:22:02 +03:00
|
|
|
goto skip;
|
|
|
|
|
2021-12-22 11:04:04 +03:00
|
|
|
nDashes = 0;
|
2024-01-30 12:25:38 +03:00
|
|
|
for (size_t index = 0; cFileName[index]; index++)
|
2022-09-26 12:22:02 +03:00
|
|
|
nDashes += (cFileName[index] == '-') ? 1 : 0;
|
2013-03-30 01:53:18 +04:00
|
|
|
|
|
|
|
if (nDashes == 1)
|
|
|
|
{
|
2024-01-23 18:49:54 +03:00
|
|
|
size_t len = 0;
|
2021-12-22 11:04:04 +03:00
|
|
|
char* p[2] = { 0 };
|
2013-03-30 01:53:18 +04:00
|
|
|
/* <name>-client.<extension> */
|
2022-09-26 12:22:02 +03:00
|
|
|
p[0] = cFileName;
|
2021-12-22 11:04:04 +03:00
|
|
|
p[1] = strchr(p[0], '-');
|
|
|
|
if (!p[1])
|
|
|
|
goto skip;
|
|
|
|
p[1] += 1;
|
|
|
|
|
|
|
|
len = (size_t)(p[1] - p[0]);
|
|
|
|
if (len < 1)
|
|
|
|
{
|
2022-09-26 12:22:02 +03:00
|
|
|
WLog_WARN(TAG, "Skipping file '%s', invalid format", cFileName);
|
2021-12-22 11:04:04 +03:00
|
|
|
goto skip;
|
|
|
|
}
|
|
|
|
strncpy(pAddin->cName, p[0], MIN(ARRAYSIZE(pAddin->cName), len - 1));
|
|
|
|
|
2013-03-30 01:53:18 +04:00
|
|
|
pAddin->dwFlags = FREERDP_ADDIN_CLIENT;
|
|
|
|
pAddin->dwFlags |= FREERDP_ADDIN_DYNAMIC;
|
|
|
|
pAddin->dwFlags |= FREERDP_ADDIN_NAME;
|
|
|
|
ppAddins[nAddins++] = pAddin;
|
2021-12-22 11:04:04 +03:00
|
|
|
|
|
|
|
used = TRUE;
|
2013-03-30 01:53:18 +04:00
|
|
|
}
|
|
|
|
else if (nDashes == 2)
|
|
|
|
{
|
2024-01-23 18:49:54 +03:00
|
|
|
size_t len = 0;
|
2021-12-22 11:04:04 +03:00
|
|
|
char* p[4] = { 0 };
|
2013-03-30 01:53:18 +04:00
|
|
|
/* <name>-client-<subsystem>.<extension> */
|
2022-09-26 12:22:02 +03:00
|
|
|
p[0] = cFileName;
|
2021-12-22 11:04:04 +03:00
|
|
|
p[1] = strchr(p[0], '-');
|
|
|
|
if (!p[1])
|
|
|
|
goto skip;
|
|
|
|
p[1] += 1;
|
|
|
|
p[2] = strchr(p[1], '-');
|
|
|
|
if (!p[2])
|
|
|
|
goto skip;
|
|
|
|
p[2] += 1;
|
|
|
|
p[3] = strchr(p[2], '.');
|
|
|
|
if (!p[3])
|
|
|
|
goto skip;
|
|
|
|
p[3] += 1;
|
|
|
|
|
|
|
|
len = (size_t)(p[1] - p[0]);
|
|
|
|
if (len < 1)
|
|
|
|
{
|
2022-09-26 12:22:02 +03:00
|
|
|
WLog_WARN(TAG, "Skipping file '%s', invalid format", cFileName);
|
2021-12-22 11:04:04 +03:00
|
|
|
goto skip;
|
|
|
|
}
|
|
|
|
strncpy(pAddin->cName, p[0], MIN(ARRAYSIZE(pAddin->cName), len - 1));
|
|
|
|
|
|
|
|
len = (size_t)(p[3] - p[2]);
|
|
|
|
if (len < 1)
|
|
|
|
{
|
2022-09-26 12:22:02 +03:00
|
|
|
WLog_WARN(TAG, "Skipping file '%s', invalid format", cFileName);
|
2021-12-22 11:04:04 +03:00
|
|
|
goto skip;
|
|
|
|
}
|
|
|
|
strncpy(pAddin->cSubsystem, p[2], MIN(ARRAYSIZE(pAddin->cSubsystem), len - 1));
|
|
|
|
|
2013-03-30 01:53:18 +04:00
|
|
|
pAddin->dwFlags = FREERDP_ADDIN_CLIENT;
|
|
|
|
pAddin->dwFlags |= FREERDP_ADDIN_DYNAMIC;
|
|
|
|
pAddin->dwFlags |= FREERDP_ADDIN_NAME;
|
|
|
|
pAddin->dwFlags |= FREERDP_ADDIN_SUBSYSTEM;
|
|
|
|
ppAddins[nAddins++] = pAddin;
|
2021-12-22 11:04:04 +03:00
|
|
|
|
|
|
|
used = TRUE;
|
2013-03-30 01:53:18 +04:00
|
|
|
}
|
|
|
|
else if (nDashes == 3)
|
|
|
|
{
|
2024-01-23 18:49:54 +03:00
|
|
|
size_t len = 0;
|
2021-12-22 11:04:04 +03:00
|
|
|
char* p[5] = { 0 };
|
2013-03-30 01:53:18 +04:00
|
|
|
/* <name>-client-<subsystem>-<type>.<extension> */
|
2022-09-26 12:22:02 +03:00
|
|
|
p[0] = cFileName;
|
2021-12-22 11:04:04 +03:00
|
|
|
p[1] = strchr(p[0], '-');
|
|
|
|
if (!p[1])
|
|
|
|
goto skip;
|
|
|
|
p[1] += 1;
|
|
|
|
p[2] = strchr(p[1], '-');
|
|
|
|
if (!p[2])
|
|
|
|
goto skip;
|
|
|
|
p[2] += 1;
|
|
|
|
p[3] = strchr(p[2], '-');
|
|
|
|
if (!p[3])
|
|
|
|
goto skip;
|
|
|
|
p[3] += 1;
|
|
|
|
p[4] = strchr(p[3], '.');
|
|
|
|
if (!p[4])
|
|
|
|
goto skip;
|
|
|
|
p[4] += 1;
|
|
|
|
|
|
|
|
len = (size_t)(p[1] - p[0]);
|
|
|
|
if (len < 1)
|
|
|
|
{
|
2022-09-26 12:22:02 +03:00
|
|
|
WLog_WARN(TAG, "Skipping file '%s', invalid format", cFileName);
|
2021-12-22 11:04:04 +03:00
|
|
|
goto skip;
|
|
|
|
}
|
|
|
|
strncpy(pAddin->cName, p[0], MIN(ARRAYSIZE(pAddin->cName), len - 1));
|
|
|
|
|
|
|
|
len = (size_t)(p[3] - p[2]);
|
|
|
|
if (len < 1)
|
|
|
|
{
|
2022-09-26 12:22:02 +03:00
|
|
|
WLog_WARN(TAG, "Skipping file '%s', invalid format", cFileName);
|
2021-12-22 11:04:04 +03:00
|
|
|
goto skip;
|
|
|
|
}
|
|
|
|
strncpy(pAddin->cSubsystem, p[2], MIN(ARRAYSIZE(pAddin->cSubsystem), len - 1));
|
|
|
|
|
|
|
|
len = (size_t)(p[4] - p[3]);
|
|
|
|
if (len < 1)
|
|
|
|
{
|
2022-09-26 12:22:02 +03:00
|
|
|
WLog_WARN(TAG, "Skipping file '%s', invalid format", cFileName);
|
2021-12-22 11:04:04 +03:00
|
|
|
goto skip;
|
|
|
|
}
|
|
|
|
strncpy(pAddin->cType, p[3], MIN(ARRAYSIZE(pAddin->cType), len - 1));
|
|
|
|
|
2013-03-30 01:53:18 +04:00
|
|
|
pAddin->dwFlags = FREERDP_ADDIN_CLIENT;
|
|
|
|
pAddin->dwFlags |= FREERDP_ADDIN_DYNAMIC;
|
|
|
|
pAddin->dwFlags |= FREERDP_ADDIN_NAME;
|
|
|
|
pAddin->dwFlags |= FREERDP_ADDIN_SUBSYSTEM;
|
|
|
|
pAddin->dwFlags |= FREERDP_ADDIN_TYPE;
|
|
|
|
ppAddins[nAddins++] = pAddin;
|
2021-12-22 11:04:04 +03:00
|
|
|
|
|
|
|
used = TRUE;
|
2013-03-30 01:53:18 +04:00
|
|
|
}
|
2021-12-22 11:04:04 +03:00
|
|
|
|
|
|
|
skip:
|
2022-09-26 12:22:02 +03:00
|
|
|
free(cFileName);
|
2021-12-22 11:04:04 +03:00
|
|
|
if (!used)
|
2013-03-30 01:53:18 +04:00
|
|
|
free(pAddin);
|
2021-12-22 11:04:04 +03:00
|
|
|
|
2022-09-26 12:22:02 +03:00
|
|
|
} while (FindNextFileW(hFind, &FindData));
|
2013-03-30 01:53:18 +04:00
|
|
|
|
|
|
|
FindClose(hFind);
|
|
|
|
ppAddins[nAddins] = NULL;
|
|
|
|
return ppAddins;
|
2015-06-02 10:56:10 +03:00
|
|
|
error_out:
|
2018-08-17 13:41:34 +03:00
|
|
|
FindClose(hFind);
|
2015-06-02 10:56:10 +03:00
|
|
|
freerdp_channels_addin_list_free(ppAddins);
|
|
|
|
return NULL;
|
2013-03-30 01:53:18 +04:00
|
|
|
}
|
|
|
|
|
2019-02-07 16:23:14 +03:00
|
|
|
FREERDP_ADDIN** freerdp_channels_list_addins(LPCSTR pszName, LPCSTR pszSubsystem, LPCSTR pszType,
|
2019-11-06 17:24:51 +03:00
|
|
|
DWORD dwFlags)
|
2013-03-30 01:53:18 +04:00
|
|
|
{
|
|
|
|
if (dwFlags & FREERDP_ADDIN_STATIC)
|
|
|
|
return freerdp_channels_list_client_static_addins(pszName, pszSubsystem, pszType, dwFlags);
|
|
|
|
else if (dwFlags & FREERDP_ADDIN_DYNAMIC)
|
|
|
|
return freerdp_channels_list_dynamic_addins(pszName, pszSubsystem, pszType, dwFlags);
|
|
|
|
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
void freerdp_channels_addin_list_free(FREERDP_ADDIN** ppAddins)
|
|
|
|
{
|
2015-06-02 10:56:10 +03:00
|
|
|
if (!ppAddins)
|
|
|
|
return;
|
|
|
|
|
2024-01-30 12:25:38 +03:00
|
|
|
for (size_t index = 0; ppAddins[index] != NULL; index++)
|
2013-03-30 01:53:18 +04:00
|
|
|
free(ppAddins[index]);
|
|
|
|
|
|
|
|
free(ppAddins);
|
|
|
|
}
|
|
|
|
|
2016-11-16 17:52:24 +03:00
|
|
|
extern const STATIC_ENTRY CLIENT_VirtualChannelEntryEx_TABLE[];
|
|
|
|
|
2019-11-20 13:30:14 +03:00
|
|
|
static BOOL freerdp_channels_is_virtual_channel_entry_ex(LPCSTR pszName)
|
2016-11-16 17:52:24 +03:00
|
|
|
{
|
2024-01-30 12:25:38 +03:00
|
|
|
for (size_t i = 0; CLIENT_VirtualChannelEntryEx_TABLE[i].name != NULL; i++)
|
2016-11-16 17:52:24 +03:00
|
|
|
{
|
2019-11-07 09:41:48 +03:00
|
|
|
const STATIC_ENTRY* entry = &CLIENT_VirtualChannelEntryEx_TABLE[i];
|
2016-11-16 17:52:24 +03:00
|
|
|
|
2019-11-07 09:41:48 +03:00
|
|
|
if (!strncmp(entry->name, pszName, MAX_PATH))
|
2016-11-16 17:52:24 +03:00
|
|
|
return TRUE;
|
|
|
|
}
|
|
|
|
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
|
2019-02-07 16:23:14 +03:00
|
|
|
PVIRTUALCHANNELENTRY freerdp_channels_load_static_addin_entry(LPCSTR pszName, LPCSTR pszSubsystem,
|
2019-11-06 17:24:51 +03:00
|
|
|
LPCSTR pszType, DWORD dwFlags)
|
2013-03-30 01:53:18 +04:00
|
|
|
{
|
2019-09-04 18:15:51 +03:00
|
|
|
const STATIC_ADDIN_TABLE* table = CLIENT_STATIC_ADDIN_TABLE;
|
2020-02-27 18:54:17 +03:00
|
|
|
const char* type = NULL;
|
|
|
|
|
2019-11-07 09:41:48 +03:00
|
|
|
if (!pszName)
|
|
|
|
return NULL;
|
|
|
|
|
2020-02-27 18:54:17 +03:00
|
|
|
if (dwFlags & FREERDP_ADDIN_CHANNEL_DYNAMIC)
|
|
|
|
type = "DVCPluginEntry";
|
|
|
|
else if (dwFlags & FREERDP_ADDIN_CHANNEL_DEVICE)
|
|
|
|
type = "DeviceServiceEntry";
|
|
|
|
else if (dwFlags & FREERDP_ADDIN_CHANNEL_STATIC)
|
|
|
|
{
|
|
|
|
if (dwFlags & FREERDP_ADDIN_CHANNEL_ENTRYEX)
|
|
|
|
type = "VirtualChannelEntryEx";
|
|
|
|
else
|
|
|
|
type = "VirtualChannelEntry";
|
|
|
|
}
|
|
|
|
|
2019-09-04 18:15:51 +03:00
|
|
|
for (; table->name != NULL; table++)
|
2013-03-30 01:53:18 +04:00
|
|
|
{
|
2019-11-07 09:41:48 +03:00
|
|
|
if (strncmp(table->name, pszName, MAX_PATH) == 0)
|
2013-03-30 01:53:18 +04:00
|
|
|
{
|
2020-02-27 18:54:17 +03:00
|
|
|
if (type && strncmp(table->type, type, MAX_PATH))
|
|
|
|
continue;
|
|
|
|
|
2013-03-30 01:53:18 +04:00
|
|
|
if (pszSubsystem != NULL)
|
|
|
|
{
|
2019-11-07 09:41:48 +03:00
|
|
|
const STATIC_SUBSYSTEM_ENTRY* subsystems = table->table;
|
2013-03-30 01:53:18 +04:00
|
|
|
|
2019-09-04 18:15:51 +03:00
|
|
|
for (; subsystems->name != NULL; subsystems++)
|
2013-03-30 01:53:18 +04:00
|
|
|
{
|
2019-09-04 18:15:51 +03:00
|
|
|
/* If the pszSubsystem is an empty string use the default backend. */
|
2019-11-07 09:41:48 +03:00
|
|
|
if ((strnlen(pszSubsystem, 1) ==
|
|
|
|
0) || /* we only want to know if strnlen is > 0 */
|
|
|
|
(strncmp(subsystems->name, pszSubsystem, MAX_PATH) == 0))
|
2013-03-30 01:53:18 +04:00
|
|
|
{
|
|
|
|
if (pszType)
|
|
|
|
{
|
2019-11-07 09:41:48 +03:00
|
|
|
if (strncmp(subsystems->type, pszType, MAX_PATH) == 0)
|
2019-11-06 17:24:51 +03:00
|
|
|
return (PVIRTUALCHANNELENTRY)subsystems->entry;
|
2013-03-30 01:53:18 +04:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2019-11-06 17:24:51 +03:00
|
|
|
return (PVIRTUALCHANNELENTRY)subsystems->entry;
|
2013-03-30 01:53:18 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2016-11-16 17:52:24 +03:00
|
|
|
if (dwFlags & FREERDP_ADDIN_CHANNEL_ENTRYEX)
|
|
|
|
{
|
|
|
|
if (!freerdp_channels_is_virtual_channel_entry_ex(pszName))
|
|
|
|
return NULL;
|
|
|
|
}
|
2017-02-20 20:31:58 +03:00
|
|
|
|
2019-11-06 17:24:51 +03:00
|
|
|
return (PVIRTUALCHANNELENTRY)table->entry;
|
2013-03-30 01:53:18 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return NULL;
|
|
|
|
}
|
2021-03-12 18:54:45 +03:00
|
|
|
|
|
|
|
typedef struct
|
|
|
|
{
|
|
|
|
wMessageQueue* queue;
|
|
|
|
wStream* data_in;
|
|
|
|
HANDLE thread;
|
|
|
|
char* channel_name;
|
|
|
|
rdpContext* ctx;
|
|
|
|
LPVOID userdata;
|
|
|
|
MsgHandler msg_handler;
|
|
|
|
} msg_proc_internals;
|
|
|
|
|
|
|
|
static DWORD WINAPI channel_client_thread_proc(LPVOID userdata)
|
|
|
|
{
|
|
|
|
UINT error = CHANNEL_RC_OK;
|
2023-06-05 13:16:57 +03:00
|
|
|
wStream* data = NULL;
|
|
|
|
wMessage message = { 0 };
|
2021-03-12 18:54:45 +03:00
|
|
|
msg_proc_internals* internals = userdata;
|
2022-04-28 09:00:39 +03:00
|
|
|
|
|
|
|
WINPR_ASSERT(internals);
|
|
|
|
|
2021-03-12 18:54:45 +03:00
|
|
|
while (1)
|
|
|
|
{
|
|
|
|
if (!MessageQueue_Wait(internals->queue))
|
|
|
|
{
|
|
|
|
WLog_ERR(TAG, "MessageQueue_Wait failed!");
|
|
|
|
error = ERROR_INTERNAL_ERROR;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (!MessageQueue_Peek(internals->queue, &message, TRUE))
|
|
|
|
{
|
|
|
|
WLog_ERR(TAG, "MessageQueue_Peek failed!");
|
|
|
|
error = ERROR_INTERNAL_ERROR;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (message.id == WMQ_QUIT)
|
|
|
|
break;
|
|
|
|
|
|
|
|
if (message.id == 0)
|
|
|
|
{
|
|
|
|
data = (wStream*)message.wParam;
|
|
|
|
|
|
|
|
if ((error = internals->msg_handler(internals->userdata, data)))
|
|
|
|
{
|
|
|
|
WLog_ERR(TAG, "msg_handler failed with error %" PRIu32 "!", error);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (error && internals->ctx)
|
|
|
|
{
|
|
|
|
char msg[128];
|
|
|
|
_snprintf(msg, 127,
|
|
|
|
"%s_virtual_channel_client_thread reported an"
|
|
|
|
" error",
|
|
|
|
internals->channel_name);
|
|
|
|
setChannelError(internals->ctx, error, msg);
|
|
|
|
}
|
|
|
|
ExitThread(error);
|
|
|
|
return error;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void free_msg(void* obj)
|
|
|
|
{
|
|
|
|
wMessage* msg = (wMessage*)obj;
|
|
|
|
|
2022-12-06 15:28:08 +03:00
|
|
|
if (msg && (msg->id == 0))
|
2021-03-12 18:54:45 +03:00
|
|
|
{
|
|
|
|
wStream* s = (wStream*)msg->wParam;
|
|
|
|
Stream_Free(s, TRUE);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-24 10:21:47 +03:00
|
|
|
static void channel_client_handler_free(msg_proc_internals* internals)
|
|
|
|
{
|
|
|
|
if (!internals)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (internals->thread)
|
|
|
|
CloseHandle(internals->thread);
|
|
|
|
MessageQueue_Free(internals->queue);
|
|
|
|
Stream_Free(internals->data_in, TRUE);
|
|
|
|
free(internals->channel_name);
|
|
|
|
free(internals);
|
|
|
|
}
|
|
|
|
|
2021-03-12 18:54:45 +03:00
|
|
|
/* Create message queue and thread or not, depending on settings */
|
|
|
|
void* channel_client_create_handler(rdpContext* ctx, LPVOID userdata, MsgHandler msg_handler,
|
|
|
|
const char* channel_name)
|
|
|
|
{
|
|
|
|
msg_proc_internals* internals = calloc(1, sizeof(msg_proc_internals));
|
|
|
|
if (!internals)
|
|
|
|
{
|
|
|
|
WLog_ERR(TAG, "calloc failed!");
|
2024-01-24 10:21:47 +03:00
|
|
|
return NULL;
|
2021-03-12 18:54:45 +03:00
|
|
|
}
|
|
|
|
internals->msg_handler = msg_handler;
|
|
|
|
internals->userdata = userdata;
|
2024-01-24 10:21:47 +03:00
|
|
|
if (channel_name)
|
|
|
|
{
|
|
|
|
internals->channel_name = _strdup(channel_name);
|
|
|
|
if (!internals->channel_name)
|
|
|
|
goto fail;
|
|
|
|
}
|
2022-06-22 13:36:09 +03:00
|
|
|
WINPR_ASSERT(ctx);
|
|
|
|
WINPR_ASSERT(ctx->settings);
|
2021-03-12 18:54:45 +03:00
|
|
|
internals->ctx = ctx;
|
2023-10-13 10:32:46 +03:00
|
|
|
if ((freerdp_settings_get_uint32(ctx->settings, FreeRDP_ThreadingFlags) &
|
|
|
|
THREADING_FLAGS_DISABLE_THREADS) == 0)
|
2021-03-12 18:54:45 +03:00
|
|
|
{
|
|
|
|
wObject obj = { 0 };
|
|
|
|
obj.fnObjectFree = free_msg;
|
|
|
|
internals->queue = MessageQueue_New(&obj);
|
|
|
|
if (!internals->queue)
|
|
|
|
{
|
|
|
|
WLog_ERR(TAG, "MessageQueue_New failed!");
|
2024-01-24 10:21:47 +03:00
|
|
|
goto fail;
|
2021-03-12 18:54:45 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!(internals->thread =
|
|
|
|
CreateThread(NULL, 0, channel_client_thread_proc, (void*)internals, 0, NULL)))
|
|
|
|
{
|
|
|
|
WLog_ERR(TAG, "CreateThread failed!");
|
2024-01-24 10:21:47 +03:00
|
|
|
goto fail;
|
2021-03-12 18:54:45 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return internals;
|
2024-01-24 10:21:47 +03:00
|
|
|
|
|
|
|
fail:
|
|
|
|
channel_client_handler_free(internals);
|
|
|
|
return NULL;
|
2021-03-12 18:54:45 +03:00
|
|
|
}
|
|
|
|
/* post a message in the queue or directly call the processing handler */
|
|
|
|
UINT channel_client_post_message(void* MsgsHandle, LPVOID pData, UINT32 dataLength,
|
|
|
|
UINT32 totalLength, UINT32 dataFlags)
|
|
|
|
{
|
|
|
|
msg_proc_internals* internals = MsgsHandle;
|
2024-01-23 18:49:54 +03:00
|
|
|
wStream* data_in = NULL;
|
2021-03-12 18:54:45 +03:00
|
|
|
|
|
|
|
if (!internals)
|
|
|
|
{
|
|
|
|
/* TODO: return some error here */
|
|
|
|
return CHANNEL_RC_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((dataFlags & CHANNEL_FLAG_SUSPEND) || (dataFlags & CHANNEL_FLAG_RESUME))
|
|
|
|
{
|
|
|
|
return CHANNEL_RC_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (dataFlags & CHANNEL_FLAG_FIRST)
|
|
|
|
{
|
|
|
|
if (internals->data_in)
|
2024-01-24 10:21:47 +03:00
|
|
|
{
|
|
|
|
if (!Stream_EnsureCapacity(internals->data_in, totalLength))
|
|
|
|
return CHANNEL_RC_NO_MEMORY;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
internals->data_in = Stream_New(NULL, totalLength);
|
2021-03-12 18:54:45 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!(data_in = internals->data_in))
|
|
|
|
{
|
|
|
|
WLog_ERR(TAG, "Stream_New failed!");
|
|
|
|
return CHANNEL_RC_NO_MEMORY;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!Stream_EnsureRemainingCapacity(data_in, dataLength))
|
|
|
|
{
|
|
|
|
Stream_Free(internals->data_in, TRUE);
|
|
|
|
internals->data_in = NULL;
|
|
|
|
return CHANNEL_RC_NO_MEMORY;
|
|
|
|
}
|
|
|
|
|
|
|
|
Stream_Write(data_in, pData, dataLength);
|
|
|
|
|
|
|
|
if (dataFlags & CHANNEL_FLAG_LAST)
|
|
|
|
{
|
|
|
|
if (Stream_Capacity(data_in) != Stream_GetPosition(data_in))
|
|
|
|
{
|
|
|
|
char msg[128];
|
|
|
|
_snprintf(msg, 127, "%s_plugin_process_received: read error", internals->channel_name);
|
|
|
|
WLog_ERR(TAG, msg);
|
|
|
|
return ERROR_INTERNAL_ERROR;
|
|
|
|
}
|
|
|
|
|
|
|
|
internals->data_in = NULL;
|
|
|
|
Stream_SealLength(data_in);
|
|
|
|
Stream_SetPosition(data_in, 0);
|
|
|
|
|
2023-10-13 10:32:46 +03:00
|
|
|
if ((freerdp_settings_get_uint32(internals->ctx->settings, FreeRDP_ThreadingFlags) &
|
|
|
|
THREADING_FLAGS_DISABLE_THREADS) != 0)
|
2021-03-12 18:54:45 +03:00
|
|
|
{
|
|
|
|
UINT error = CHANNEL_RC_OK;
|
|
|
|
if ((error = internals->msg_handler(internals->userdata, data_in)))
|
|
|
|
{
|
|
|
|
WLog_ERR(TAG,
|
|
|
|
"msg_handler failed with error"
|
|
|
|
" %" PRIu32 "!",
|
|
|
|
error);
|
|
|
|
return ERROR_INTERNAL_ERROR;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (!MessageQueue_Post(internals->queue, NULL, 0, (void*)data_in, NULL))
|
|
|
|
{
|
|
|
|
WLog_ERR(TAG, "MessageQueue_Post failed!");
|
|
|
|
return ERROR_INTERNAL_ERROR;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return CHANNEL_RC_OK;
|
|
|
|
}
|
|
|
|
/* Tear down queue and thread */
|
|
|
|
UINT channel_client_quit_handler(void* MsgsHandle)
|
|
|
|
{
|
|
|
|
msg_proc_internals* internals = MsgsHandle;
|
2024-01-23 18:49:54 +03:00
|
|
|
UINT rc = 0;
|
2021-03-12 18:54:45 +03:00
|
|
|
if (!internals)
|
|
|
|
{
|
|
|
|
/* TODO: return some error here */
|
|
|
|
return CHANNEL_RC_OK;
|
|
|
|
}
|
2021-09-06 17:23:31 +03:00
|
|
|
|
|
|
|
WINPR_ASSERT(internals->ctx);
|
|
|
|
WINPR_ASSERT(internals->ctx->settings);
|
2021-09-06 17:46:01 +03:00
|
|
|
|
2023-10-13 10:32:46 +03:00
|
|
|
if ((freerdp_settings_get_uint32(internals->ctx->settings, FreeRDP_ThreadingFlags) &
|
|
|
|
THREADING_FLAGS_DISABLE_THREADS) == 0)
|
2021-03-12 18:54:45 +03:00
|
|
|
{
|
2021-09-06 17:46:01 +03:00
|
|
|
if (internals->queue && internals->thread)
|
2021-03-12 18:54:45 +03:00
|
|
|
{
|
2021-09-06 17:46:01 +03:00
|
|
|
if (MessageQueue_PostQuit(internals->queue, 0) &&
|
|
|
|
(WaitForSingleObject(internals->thread, INFINITE) == WAIT_FAILED))
|
|
|
|
{
|
|
|
|
rc = GetLastError();
|
|
|
|
WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", rc);
|
|
|
|
return rc;
|
|
|
|
}
|
2021-03-12 18:54:45 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-24 10:21:47 +03:00
|
|
|
channel_client_handler_free(internals);
|
2021-03-12 18:54:45 +03:00
|
|
|
return CHANNEL_RC_OK;
|
|
|
|
}
|