2014-07-11 00:32:46 +04:00
|
|
|
/**
|
|
|
|
* FreeRDP: A Remote Desktop Protocol Implementation
|
|
|
|
*
|
|
|
|
* Copyright 2014 Marc-Andre Moreau <marcandre.moreau@gmail.com>
|
|
|
|
*
|
|
|
|
* 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.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
|
|
#include "config.h"
|
|
|
|
#endif
|
|
|
|
|
2014-07-14 21:33:20 +04:00
|
|
|
#include <winpr/crt.h>
|
2014-08-15 21:50:22 +04:00
|
|
|
#include <winpr/ssl.h>
|
2014-08-14 21:10:43 +04:00
|
|
|
#include <winpr/wnd.h>
|
2014-07-18 05:15:22 +04:00
|
|
|
#include <winpr/path.h>
|
2014-07-14 21:33:20 +04:00
|
|
|
#include <winpr/cmdline.h>
|
2014-07-18 05:15:22 +04:00
|
|
|
#include <winpr/winsock.h>
|
2014-07-14 21:33:20 +04:00
|
|
|
|
2014-09-12 19:38:12 +04:00
|
|
|
#include <freerdp/log.h>
|
2014-09-18 05:18:47 +04:00
|
|
|
#include <freerdp/version.h>
|
2014-07-14 21:33:20 +04:00
|
|
|
|
2014-07-19 01:26:21 +04:00
|
|
|
#include <winpr/tools/makecert.h>
|
|
|
|
|
2014-07-12 07:01:34 +04:00
|
|
|
#ifndef _WIN32
|
|
|
|
#include <sys/select.h>
|
2015-02-02 19:48:54 +03:00
|
|
|
#include <signal.h>
|
2014-07-12 07:01:34 +04:00
|
|
|
#endif
|
|
|
|
|
2014-07-11 00:32:46 +04:00
|
|
|
#include "shadow.h"
|
|
|
|
|
2014-09-12 19:38:12 +04:00
|
|
|
#define TAG SERVER_TAG("shadow")
|
|
|
|
|
2014-07-14 21:33:20 +04:00
|
|
|
static COMMAND_LINE_ARGUMENT_A shadow_args[] =
|
|
|
|
{
|
|
|
|
{ "port", COMMAND_LINE_VALUE_REQUIRED, "<number>", NULL, NULL, -1, NULL, "Server port" },
|
2014-08-06 20:08:00 +04:00
|
|
|
{ "ipc-socket", COMMAND_LINE_VALUE_REQUIRED, "<ipc-socket>", NULL, NULL, -1, NULL, "Server IPC socket" },
|
2014-07-14 21:33:20 +04:00
|
|
|
{ "monitors", COMMAND_LINE_VALUE_OPTIONAL, "<0,1,2...>", NULL, NULL, -1, NULL, "Select or list monitors" },
|
2014-09-11 00:27:24 +04:00
|
|
|
{ "rect", COMMAND_LINE_VALUE_REQUIRED, "<x,y,w,h>", NULL, NULL, -1, NULL, "Select rectangle within monitor to share" },
|
2014-09-27 03:03:48 +04:00
|
|
|
{ "auth", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueFalse, NULL, -1, NULL, "Clients must authenticate" },
|
2014-07-15 02:01:29 +04:00
|
|
|
{ "may-view", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueTrue, NULL, -1, NULL, "Clients may view without prompt" },
|
|
|
|
{ "may-interact", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueTrue, NULL, -1, NULL, "Clients may interact without prompt" },
|
2016-07-22 00:53:20 +03:00
|
|
|
{ "sec", COMMAND_LINE_VALUE_REQUIRED, "<rdp|tls|nla|ext>", NULL, NULL, -1, NULL, "force specific protocol security" },
|
|
|
|
{ "sec-rdp", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueTrue, NULL, -1, NULL, "rdp protocol security" },
|
|
|
|
{ "sec-tls", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueTrue, NULL, -1, NULL, "tls protocol security" },
|
|
|
|
{ "sec-nla", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueTrue, NULL, -1, NULL, "nla protocol security" },
|
|
|
|
{ "sec-ext", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueFalse, NULL, -1, NULL, "nla extended protocol security" },
|
2016-07-22 01:58:24 +03:00
|
|
|
{ "sam-file", COMMAND_LINE_VALUE_REQUIRED, "<file>", NULL, NULL, -1, NULL, "NTLM SAM file for NLA authentication" },
|
2014-07-14 21:33:20 +04:00
|
|
|
{ "version", COMMAND_LINE_VALUE_FLAG | COMMAND_LINE_PRINT_VERSION, NULL, NULL, NULL, -1, NULL, "Print version" },
|
|
|
|
{ "help", COMMAND_LINE_VALUE_FLAG | COMMAND_LINE_PRINT_HELP, NULL, NULL, NULL, -1, "?", "Print help" },
|
|
|
|
{ NULL, 0, NULL, NULL, NULL, -1, NULL, NULL }
|
|
|
|
};
|
|
|
|
|
|
|
|
int shadow_server_print_command_line_help(int argc, char** argv)
|
|
|
|
{
|
|
|
|
char* str;
|
|
|
|
int length;
|
|
|
|
COMMAND_LINE_ARGUMENT_A* arg;
|
|
|
|
|
2014-09-12 19:38:12 +04:00
|
|
|
WLog_INFO(TAG, "Usage: %s [options]", argv[0]);
|
|
|
|
WLog_INFO(TAG, "");
|
2014-07-14 21:33:20 +04:00
|
|
|
|
2014-09-12 19:38:12 +04:00
|
|
|
WLog_INFO(TAG, "Syntax:");
|
|
|
|
WLog_INFO(TAG, " /flag (enables flag)");
|
|
|
|
WLog_INFO(TAG, " /option:<value> (specifies option with value)");
|
|
|
|
WLog_INFO(TAG, " +toggle -toggle (enables or disables toggle, where '/' is a synonym of '+')");
|
|
|
|
WLog_INFO(TAG, "");
|
2014-07-14 21:33:20 +04:00
|
|
|
|
|
|
|
arg = shadow_args;
|
|
|
|
|
|
|
|
do
|
|
|
|
{
|
|
|
|
if (arg->Flags & COMMAND_LINE_VALUE_FLAG)
|
|
|
|
{
|
2014-09-12 19:38:12 +04:00
|
|
|
WLog_INFO(TAG, " %s", "/");
|
|
|
|
WLog_INFO(TAG, "%-20s", arg->Name);
|
|
|
|
WLog_INFO(TAG, "\t%s", arg->Text);
|
2014-07-14 21:33:20 +04:00
|
|
|
}
|
|
|
|
else if ((arg->Flags & COMMAND_LINE_VALUE_REQUIRED) || (arg->Flags & COMMAND_LINE_VALUE_OPTIONAL))
|
|
|
|
{
|
2014-09-12 19:38:12 +04:00
|
|
|
WLog_INFO(TAG, " %s", "/");
|
2014-07-14 21:33:20 +04:00
|
|
|
|
|
|
|
if (arg->Format)
|
|
|
|
{
|
|
|
|
length = (int) (strlen(arg->Name) + strlen(arg->Format) + 2);
|
|
|
|
str = (char*) malloc(length + 1);
|
2015-06-16 16:42:07 +03:00
|
|
|
if (!str)
|
2015-06-17 23:08:02 +03:00
|
|
|
return -1;
|
2014-07-14 21:33:20 +04:00
|
|
|
sprintf_s(str, length + 1, "%s:%s", arg->Name, arg->Format);
|
2014-09-12 19:38:12 +04:00
|
|
|
WLog_INFO(TAG, "%-20s", str);
|
2014-07-14 21:33:20 +04:00
|
|
|
free(str);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2014-09-12 19:38:12 +04:00
|
|
|
WLog_INFO(TAG, "%-20s", arg->Name);
|
2014-07-14 21:33:20 +04:00
|
|
|
}
|
|
|
|
|
2014-09-12 19:38:12 +04:00
|
|
|
WLog_INFO(TAG, "\t%s", arg->Text);
|
2014-07-14 21:33:20 +04:00
|
|
|
}
|
|
|
|
else if (arg->Flags & COMMAND_LINE_VALUE_BOOL)
|
|
|
|
{
|
|
|
|
length = (int) strlen(arg->Name) + 32;
|
|
|
|
str = (char*) malloc(length + 1);
|
2015-06-17 23:08:02 +03:00
|
|
|
if (!str)
|
|
|
|
return -1;
|
2014-07-14 21:33:20 +04:00
|
|
|
sprintf_s(str, length + 1, "%s (default:%s)", arg->Name,
|
|
|
|
arg->Default ? "on" : "off");
|
|
|
|
|
2014-09-12 19:38:12 +04:00
|
|
|
WLog_INFO(TAG, " %s", arg->Default ? "-" : "+");
|
2014-07-14 21:33:20 +04:00
|
|
|
|
2014-09-12 19:38:12 +04:00
|
|
|
WLog_INFO(TAG, "%-20s", str);
|
2014-07-14 21:33:20 +04:00
|
|
|
free(str);
|
|
|
|
|
2014-09-12 19:38:12 +04:00
|
|
|
WLog_INFO(TAG, "\t%s", arg->Text);
|
2014-07-14 21:33:20 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
while ((arg = CommandLineFindNextArgumentA(arg)) != NULL);
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
int shadow_server_command_line_status_print(rdpShadowServer* server, int argc, char** argv, int status)
|
|
|
|
{
|
|
|
|
if (status == COMMAND_LINE_STATUS_PRINT_VERSION)
|
|
|
|
{
|
2014-09-12 19:38:12 +04:00
|
|
|
WLog_INFO(TAG, "FreeRDP version %s (git %s)", FREERDP_VERSION_FULL, GIT_REVISION);
|
2014-07-14 21:33:20 +04:00
|
|
|
return COMMAND_LINE_STATUS_PRINT_VERSION;
|
|
|
|
}
|
|
|
|
else if (status == COMMAND_LINE_STATUS_PRINT)
|
|
|
|
{
|
|
|
|
return COMMAND_LINE_STATUS_PRINT;
|
|
|
|
}
|
|
|
|
else if (status < 0)
|
|
|
|
{
|
2015-06-17 23:08:02 +03:00
|
|
|
if (shadow_server_print_command_line_help(argc, argv) < 0)
|
|
|
|
return -1;
|
2014-07-14 21:33:20 +04:00
|
|
|
return COMMAND_LINE_STATUS_PRINT_HELP;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
int shadow_server_parse_command_line(rdpShadowServer* server, int argc, char** argv)
|
|
|
|
{
|
|
|
|
int status;
|
|
|
|
DWORD flags;
|
|
|
|
COMMAND_LINE_ARGUMENT_A* arg;
|
2016-07-22 00:53:20 +03:00
|
|
|
rdpSettings* settings = server->settings;
|
2014-07-14 21:33:20 +04:00
|
|
|
|
|
|
|
if (argc < 2)
|
|
|
|
return 1;
|
|
|
|
|
|
|
|
CommandLineClearArgumentsA(shadow_args);
|
|
|
|
|
|
|
|
flags = COMMAND_LINE_SEPARATOR_COLON;
|
|
|
|
flags |= COMMAND_LINE_SIGIL_SLASH | COMMAND_LINE_SIGIL_PLUS_MINUS;
|
|
|
|
|
|
|
|
status = CommandLineParseArgumentsA(argc, (const char**) argv, shadow_args, flags, server, NULL, NULL);
|
|
|
|
|
|
|
|
if (status < 0)
|
|
|
|
return status;
|
|
|
|
|
|
|
|
arg = shadow_args;
|
|
|
|
|
|
|
|
do
|
|
|
|
{
|
|
|
|
if (!(arg->Flags & COMMAND_LINE_ARGUMENT_PRESENT))
|
|
|
|
continue;
|
|
|
|
|
|
|
|
CommandLineSwitchStart(arg)
|
|
|
|
|
|
|
|
CommandLineSwitchCase(arg, "port")
|
|
|
|
{
|
|
|
|
server->port = (DWORD) atoi(arg->Value);
|
|
|
|
}
|
2014-08-06 20:08:00 +04:00
|
|
|
CommandLineSwitchCase(arg, "ipc-socket")
|
|
|
|
{
|
|
|
|
server->ipcSocket = _strdup(arg->Value);
|
2015-06-17 23:08:02 +03:00
|
|
|
if (!server->ipcSocket)
|
|
|
|
return -1;
|
2014-08-06 20:08:00 +04:00
|
|
|
}
|
2014-07-15 02:01:29 +04:00
|
|
|
CommandLineSwitchCase(arg, "may-view")
|
|
|
|
{
|
|
|
|
server->mayView = arg->Value ? TRUE : FALSE;
|
|
|
|
}
|
|
|
|
CommandLineSwitchCase(arg, "may-interact")
|
|
|
|
{
|
|
|
|
server->mayInteract = arg->Value ? TRUE : FALSE;
|
|
|
|
}
|
2014-09-11 00:27:24 +04:00
|
|
|
CommandLineSwitchCase(arg, "rect")
|
|
|
|
{
|
|
|
|
char* p;
|
|
|
|
char* tok[4];
|
|
|
|
int x, y, w, h;
|
|
|
|
char* str = _strdup(arg->Value);
|
|
|
|
if (!str)
|
|
|
|
return -1;
|
|
|
|
|
|
|
|
tok[0] = p = str;
|
|
|
|
|
|
|
|
p = strchr(p + 1, ',');
|
|
|
|
|
|
|
|
if (!p)
|
2016-05-12 11:01:30 +03:00
|
|
|
{
|
|
|
|
free(str);
|
2014-09-11 00:27:24 +04:00
|
|
|
return -1;
|
2016-05-12 11:01:30 +03:00
|
|
|
}
|
2014-09-11 00:27:24 +04:00
|
|
|
|
|
|
|
*p++ = '\0';
|
|
|
|
tok[1] = p;
|
|
|
|
|
|
|
|
p = strchr(p + 1, ',');
|
|
|
|
|
|
|
|
if (!p)
|
2016-05-12 11:01:30 +03:00
|
|
|
{
|
|
|
|
free(str);
|
2014-09-11 00:27:24 +04:00
|
|
|
return -1;
|
2016-05-12 11:01:30 +03:00
|
|
|
}
|
2014-09-11 00:27:24 +04:00
|
|
|
|
|
|
|
*p++ = '\0';
|
|
|
|
tok[2] = p;
|
|
|
|
|
|
|
|
p = strchr(p + 1, ',');
|
|
|
|
|
|
|
|
if (!p)
|
2016-05-12 11:01:30 +03:00
|
|
|
{
|
|
|
|
free(str);
|
2014-09-11 00:27:24 +04:00
|
|
|
return -1;
|
2016-05-12 11:01:30 +03:00
|
|
|
}
|
2014-09-11 00:27:24 +04:00
|
|
|
|
|
|
|
*p++ = '\0';
|
|
|
|
tok[3] = p;
|
|
|
|
|
|
|
|
x = atoi(tok[0]);
|
|
|
|
y = atoi(tok[1]);
|
|
|
|
w = atoi(tok[2]);
|
|
|
|
h = atoi(tok[3]);
|
2016-05-12 11:01:30 +03:00
|
|
|
free(str);
|
2014-09-11 00:27:24 +04:00
|
|
|
|
|
|
|
if ((x < 0) || (y < 0) || (w < 1) || (h < 1))
|
|
|
|
return -1;
|
|
|
|
|
|
|
|
server->subRect.left = x;
|
|
|
|
server->subRect.top = y;
|
|
|
|
server->subRect.right = x + w;
|
|
|
|
server->subRect.bottom = y + h;
|
|
|
|
server->shareSubRect = TRUE;
|
|
|
|
}
|
2014-09-27 03:03:48 +04:00
|
|
|
CommandLineSwitchCase(arg, "auth")
|
|
|
|
{
|
|
|
|
server->authentication = arg->Value ? TRUE : FALSE;
|
|
|
|
}
|
2016-07-22 00:53:20 +03:00
|
|
|
CommandLineSwitchCase(arg, "sec")
|
|
|
|
{
|
|
|
|
if (strcmp("rdp", arg->Value) == 0) /* Standard RDP */
|
|
|
|
{
|
|
|
|
settings->RdpSecurity = TRUE;
|
|
|
|
settings->TlsSecurity = FALSE;
|
|
|
|
settings->NlaSecurity = FALSE;
|
|
|
|
settings->ExtSecurity = FALSE;
|
|
|
|
settings->UseRdpSecurityLayer = TRUE;
|
|
|
|
}
|
|
|
|
else if (strcmp("tls", arg->Value) == 0) /* TLS */
|
|
|
|
{
|
|
|
|
settings->RdpSecurity = FALSE;
|
|
|
|
settings->TlsSecurity = TRUE;
|
|
|
|
settings->NlaSecurity = FALSE;
|
|
|
|
settings->ExtSecurity = FALSE;
|
|
|
|
}
|
|
|
|
else if (strcmp("nla", arg->Value) == 0) /* NLA */
|
|
|
|
{
|
|
|
|
settings->RdpSecurity = FALSE;
|
|
|
|
settings->TlsSecurity = FALSE;
|
|
|
|
settings->NlaSecurity = TRUE;
|
|
|
|
settings->ExtSecurity = FALSE;
|
|
|
|
}
|
|
|
|
else if (strcmp("ext", arg->Value) == 0) /* NLA Extended */
|
|
|
|
{
|
|
|
|
settings->RdpSecurity = FALSE;
|
|
|
|
settings->TlsSecurity = FALSE;
|
|
|
|
settings->NlaSecurity = FALSE;
|
|
|
|
settings->ExtSecurity = TRUE;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
WLog_ERR(TAG, "unknown protocol security: %s", arg->Value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
CommandLineSwitchCase(arg, "sec-rdp")
|
|
|
|
{
|
|
|
|
settings->RdpSecurity = arg->Value ? TRUE : FALSE;
|
|
|
|
}
|
|
|
|
CommandLineSwitchCase(arg, "sec-tls")
|
|
|
|
{
|
|
|
|
settings->TlsSecurity = arg->Value ? TRUE : FALSE;
|
|
|
|
}
|
|
|
|
CommandLineSwitchCase(arg, "sec-nla")
|
|
|
|
{
|
|
|
|
settings->NlaSecurity = arg->Value ? TRUE : FALSE;
|
|
|
|
}
|
|
|
|
CommandLineSwitchCase(arg, "sec-ext")
|
|
|
|
{
|
|
|
|
settings->ExtSecurity = arg->Value ? TRUE : FALSE;
|
|
|
|
}
|
2016-07-22 01:58:24 +03:00
|
|
|
CommandLineSwitchCase(arg, "sam-file")
|
|
|
|
{
|
|
|
|
freerdp_set_param_string(settings, FreeRDP_NtlmSamFile, arg->Value);
|
|
|
|
}
|
2014-07-14 21:33:20 +04:00
|
|
|
CommandLineSwitchDefault(arg)
|
|
|
|
{
|
2014-07-15 02:01:29 +04:00
|
|
|
|
2014-07-14 21:33:20 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
CommandLineSwitchEnd(arg)
|
|
|
|
}
|
|
|
|
while ((arg = CommandLineFindNextArgumentA(arg)) != NULL);
|
|
|
|
|
|
|
|
arg = CommandLineFindArgumentA(shadow_args, "monitors");
|
|
|
|
|
2014-07-15 02:01:29 +04:00
|
|
|
if (arg && (arg->Flags & COMMAND_LINE_ARGUMENT_PRESENT))
|
2014-07-14 21:33:20 +04:00
|
|
|
{
|
2014-09-10 22:58:14 +04:00
|
|
|
int index;
|
2014-09-18 21:06:49 +04:00
|
|
|
int numMonitors;
|
|
|
|
MONITOR_DEF monitors[16];
|
|
|
|
|
2015-04-08 20:30:57 +03:00
|
|
|
numMonitors = shadow_enum_monitors(monitors, 16);
|
2014-09-10 22:58:14 +04:00
|
|
|
|
2014-07-14 21:33:20 +04:00
|
|
|
if (arg->Flags & COMMAND_LINE_VALUE_PRESENT)
|
|
|
|
{
|
|
|
|
/* Select monitors */
|
2014-09-10 22:58:14 +04:00
|
|
|
|
|
|
|
index = atoi(arg->Value);
|
|
|
|
|
|
|
|
if (index < 0)
|
|
|
|
index = 0;
|
|
|
|
|
2014-09-18 21:06:49 +04:00
|
|
|
if (index >= numMonitors)
|
2014-09-10 22:58:14 +04:00
|
|
|
index = 0;
|
|
|
|
|
2014-09-18 21:06:49 +04:00
|
|
|
server->selectedMonitor = index;
|
2014-07-14 21:33:20 +04:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
int width, height;
|
|
|
|
MONITOR_DEF* monitor;
|
|
|
|
|
|
|
|
/* List monitors */
|
|
|
|
|
2014-09-18 21:06:49 +04:00
|
|
|
for (index = 0; index < numMonitors; index++)
|
2014-07-14 21:33:20 +04:00
|
|
|
{
|
2014-09-18 21:06:49 +04:00
|
|
|
monitor = &monitors[index];
|
2014-07-14 21:33:20 +04:00
|
|
|
|
|
|
|
width = monitor->right - monitor->left;
|
|
|
|
height = monitor->bottom - monitor->top;
|
|
|
|
|
2016-12-14 00:47:08 +03:00
|
|
|
WLog_INFO(TAG, " %s [%d] %dx%d\t+%"PRId32"+%"PRId32"",
|
2014-07-14 21:33:20 +04:00
|
|
|
(monitor->flags == 1) ? "*" : " ", index,
|
|
|
|
width, height, monitor->left, monitor->top);
|
|
|
|
}
|
|
|
|
|
|
|
|
status = COMMAND_LINE_STATUS_PRINT;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return status;
|
|
|
|
}
|
|
|
|
|
2014-07-12 03:30:40 +04:00
|
|
|
void* shadow_server_thread(rdpShadowServer* server)
|
2014-07-11 00:32:46 +04:00
|
|
|
{
|
2014-07-11 01:20:41 +04:00
|
|
|
DWORD status;
|
|
|
|
DWORD nCount;
|
|
|
|
HANDLE events[32];
|
2014-07-12 09:18:08 +04:00
|
|
|
HANDLE StopEvent;
|
2014-07-11 01:20:41 +04:00
|
|
|
freerdp_listener* listener;
|
2014-07-14 05:20:36 +04:00
|
|
|
rdpShadowSubsystem* subsystem;
|
2014-07-11 01:20:41 +04:00
|
|
|
|
2014-07-12 03:30:40 +04:00
|
|
|
listener = server->listener;
|
2014-07-12 09:18:08 +04:00
|
|
|
StopEvent = server->StopEvent;
|
2014-07-14 05:20:36 +04:00
|
|
|
subsystem = server->subsystem;
|
2014-07-11 01:20:41 +04:00
|
|
|
|
2014-09-18 23:43:11 +04:00
|
|
|
shadow_subsystem_start(server->subsystem);
|
2014-07-14 03:42:57 +04:00
|
|
|
|
2014-07-11 01:20:41 +04:00
|
|
|
while (1)
|
|
|
|
{
|
2015-04-21 16:57:25 +03:00
|
|
|
nCount = listener->GetEventHandles(listener, events, 32);
|
|
|
|
if (0 == nCount)
|
2014-07-11 01:20:41 +04:00
|
|
|
{
|
2014-09-12 19:38:12 +04:00
|
|
|
WLog_ERR(TAG, "Failed to get FreeRDP file descriptor");
|
2014-07-11 01:20:41 +04:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2014-07-12 09:18:08 +04:00
|
|
|
events[nCount++] = server->StopEvent;
|
|
|
|
|
2014-07-11 01:20:41 +04:00
|
|
|
status = WaitForMultipleObjects(nCount, events, FALSE, INFINITE);
|
|
|
|
|
2014-07-12 09:18:08 +04:00
|
|
|
if (WaitForSingleObject(server->StopEvent, 0) == WAIT_OBJECT_0)
|
|
|
|
{
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2014-07-11 01:20:41 +04:00
|
|
|
if (!listener->CheckFileDescriptor(listener))
|
|
|
|
{
|
2014-09-12 19:38:12 +04:00
|
|
|
WLog_ERR(TAG, "Failed to check FreeRDP file descriptor");
|
2014-07-11 01:20:41 +04:00
|
|
|
break;
|
|
|
|
}
|
2014-08-16 02:12:53 +04:00
|
|
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
Sleep(100); /* FIXME: listener event handles */
|
|
|
|
#endif
|
2014-07-11 01:20:41 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
listener->Close(listener);
|
|
|
|
|
2014-09-18 23:43:11 +04:00
|
|
|
shadow_subsystem_stop(server->subsystem);
|
2014-07-14 05:20:36 +04:00
|
|
|
|
2015-05-21 15:32:10 +03:00
|
|
|
/* Signal to the clients that server is being stopped and wait for them
|
|
|
|
* to disconnect. */
|
2015-04-08 21:13:52 +03:00
|
|
|
if (shadow_client_boardcast_quit(server, 0))
|
2015-05-21 15:32:10 +03:00
|
|
|
{
|
2015-05-23 23:47:18 +03:00
|
|
|
while(ArrayList_Count(server->clients) > 0)
|
|
|
|
{
|
|
|
|
Sleep(100);
|
|
|
|
}
|
2015-05-21 15:32:10 +03:00
|
|
|
}
|
|
|
|
|
2014-07-11 00:32:46 +04:00
|
|
|
ExitThread(0);
|
|
|
|
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
2014-07-11 01:20:41 +04:00
|
|
|
int shadow_server_start(rdpShadowServer* server)
|
2014-07-11 00:32:46 +04:00
|
|
|
{
|
2014-08-06 20:08:00 +04:00
|
|
|
BOOL status;
|
2014-07-18 05:15:22 +04:00
|
|
|
WSADATA wsaData;
|
|
|
|
|
|
|
|
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
|
|
|
|
return -1;
|
|
|
|
|
2014-07-12 07:01:34 +04:00
|
|
|
#ifndef _WIN32
|
|
|
|
signal(SIGPIPE, SIG_IGN);
|
|
|
|
#endif
|
|
|
|
|
2014-09-10 22:58:14 +04:00
|
|
|
server->screen = shadow_screen_new(server);
|
|
|
|
|
|
|
|
if (!server->screen)
|
2017-01-16 10:58:04 +03:00
|
|
|
{
|
|
|
|
WLog_ERR(TAG, "screen_new failed");
|
2014-09-10 22:58:14 +04:00
|
|
|
return -1;
|
2017-01-16 10:58:04 +03:00
|
|
|
}
|
2014-09-10 22:58:14 +04:00
|
|
|
|
|
|
|
server->capture = shadow_capture_new(server);
|
|
|
|
|
|
|
|
if (!server->capture)
|
2017-01-16 10:58:04 +03:00
|
|
|
{
|
|
|
|
WLog_ERR(TAG, "capture_new failed");
|
2014-09-10 22:58:14 +04:00
|
|
|
return -1;
|
2017-01-16 10:58:04 +03:00
|
|
|
}
|
2014-09-10 22:58:14 +04:00
|
|
|
|
2014-08-06 20:08:00 +04:00
|
|
|
if (!server->ipcSocket)
|
|
|
|
status = server->listener->Open(server->listener, NULL, (UINT16) server->port);
|
|
|
|
else
|
|
|
|
status = server->listener->OpenLocal(server->listener, server->ipcSocket);
|
|
|
|
|
2015-05-05 14:55:48 +03:00
|
|
|
if (!status)
|
2017-01-16 10:58:04 +03:00
|
|
|
{
|
|
|
|
WLog_ERR(TAG, "Problem creating listener. (Port already used or insufficient permissions?)");
|
2015-05-05 14:55:48 +03:00
|
|
|
return -1;
|
2017-01-16 10:58:04 +03:00
|
|
|
}
|
2015-05-05 14:55:48 +03:00
|
|
|
|
|
|
|
if (!(server->thread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)
|
|
|
|
shadow_server_thread, (void*) server, 0, NULL)))
|
2014-07-11 00:32:46 +04:00
|
|
|
{
|
2015-05-05 14:55:48 +03:00
|
|
|
return -1;
|
2014-07-11 00:32:46 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2014-07-11 01:20:41 +04:00
|
|
|
int shadow_server_stop(rdpShadowServer* server)
|
2014-07-11 00:32:46 +04:00
|
|
|
{
|
|
|
|
if (server->thread)
|
|
|
|
{
|
2014-07-12 09:18:08 +04:00
|
|
|
SetEvent(server->StopEvent);
|
2014-07-11 00:32:46 +04:00
|
|
|
WaitForSingleObject(server->thread, INFINITE);
|
|
|
|
CloseHandle(server->thread);
|
2014-07-12 09:18:08 +04:00
|
|
|
server->thread = NULL;
|
2014-07-11 00:32:46 +04:00
|
|
|
|
|
|
|
server->listener->Close(server->listener);
|
|
|
|
}
|
|
|
|
|
2014-09-10 22:58:14 +04:00
|
|
|
if (server->screen)
|
|
|
|
{
|
|
|
|
shadow_screen_free(server->screen);
|
|
|
|
server->screen = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (server->capture)
|
|
|
|
{
|
|
|
|
shadow_capture_free(server->capture);
|
|
|
|
server->capture = NULL;
|
|
|
|
}
|
|
|
|
|
2014-07-11 00:32:46 +04:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2014-09-18 18:06:59 +04:00
|
|
|
int shadow_server_init_config_path(rdpShadowServer* server)
|
|
|
|
{
|
|
|
|
#ifdef _WIN32
|
|
|
|
if (!server->ConfigPath)
|
|
|
|
{
|
|
|
|
server->ConfigPath = GetEnvironmentSubPath("LOCALAPPDATA", "freerdp");
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#ifdef __APPLE__
|
|
|
|
if (!server->ConfigPath)
|
|
|
|
{
|
|
|
|
char* userLibraryPath;
|
|
|
|
char* userApplicationSupportPath;
|
|
|
|
|
|
|
|
userLibraryPath = GetKnownSubPath(KNOWN_PATH_HOME, "Library");
|
|
|
|
|
|
|
|
if (userLibraryPath)
|
|
|
|
{
|
2015-05-05 20:45:34 +03:00
|
|
|
if (!PathFileExistsA(userLibraryPath) &&
|
2015-06-03 12:47:40 +03:00
|
|
|
!PathMakePathA(userLibraryPath, 0))
|
2015-05-05 20:45:34 +03:00
|
|
|
{
|
2015-05-07 18:42:01 +03:00
|
|
|
WLog_ERR(TAG, "Failed to create directory '%s'", userLibraryPath);
|
2015-05-05 20:45:34 +03:00
|
|
|
free(userLibraryPath);
|
|
|
|
return -1;
|
|
|
|
}
|
2014-09-18 18:06:59 +04:00
|
|
|
|
|
|
|
userApplicationSupportPath = GetCombinedPath(userLibraryPath, "Application Support");
|
|
|
|
|
|
|
|
if (userApplicationSupportPath)
|
|
|
|
{
|
2015-05-05 20:45:34 +03:00
|
|
|
if (!PathFileExistsA(userApplicationSupportPath) &&
|
2015-06-03 12:47:40 +03:00
|
|
|
!PathMakePathA(userApplicationSupportPath, 0))
|
2015-05-05 20:45:34 +03:00
|
|
|
{
|
|
|
|
WLog_ERR(TAG, "Failed to create directory '%s'", userApplicationSupportPath);
|
|
|
|
free(userLibraryPath);
|
|
|
|
free(userApplicationSupportPath);
|
|
|
|
return -1;
|
|
|
|
}
|
2014-09-18 18:06:59 +04:00
|
|
|
server->ConfigPath = GetCombinedPath(userApplicationSupportPath, "freerdp");
|
|
|
|
}
|
|
|
|
|
|
|
|
free(userLibraryPath);
|
|
|
|
free(userApplicationSupportPath);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
if (!server->ConfigPath)
|
|
|
|
{
|
|
|
|
char* configHome;
|
|
|
|
|
|
|
|
configHome = GetKnownPath(KNOWN_PATH_XDG_CONFIG_HOME);
|
|
|
|
|
|
|
|
if (configHome)
|
|
|
|
{
|
2015-05-05 20:45:34 +03:00
|
|
|
if (!PathFileExistsA(configHome) &&
|
2015-06-03 12:47:40 +03:00
|
|
|
!PathMakePathA(configHome, 0))
|
2015-05-05 20:45:34 +03:00
|
|
|
{
|
|
|
|
WLog_ERR(TAG, "Failed to create directory '%s'", configHome);
|
|
|
|
free(configHome);
|
|
|
|
return -1;
|
|
|
|
}
|
2014-09-18 18:06:59 +04:00
|
|
|
server->ConfigPath = GetKnownSubPath(KNOWN_PATH_XDG_CONFIG_HOME, "freerdp");
|
|
|
|
free(configHome);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!server->ConfigPath)
|
|
|
|
return -1; /* no usable config path */
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2015-06-17 23:08:02 +03:00
|
|
|
static BOOL shadow_server_init_certificate(rdpShadowServer* server)
|
2014-07-19 01:26:21 +04:00
|
|
|
{
|
|
|
|
char* filepath;
|
2015-06-17 23:08:02 +03:00
|
|
|
MAKECERT_CONTEXT* makecert = NULL;
|
|
|
|
BOOL ret = FALSE;
|
2014-07-19 01:26:21 +04:00
|
|
|
|
|
|
|
const char* makecert_argv[6] =
|
|
|
|
{
|
|
|
|
"makecert",
|
|
|
|
"-rdp",
|
|
|
|
"-live",
|
|
|
|
"-silent",
|
|
|
|
"-y", "5"
|
|
|
|
};
|
|
|
|
|
|
|
|
int makecert_argc = (sizeof(makecert_argv) / sizeof(char*));
|
|
|
|
|
2015-05-05 20:45:34 +03:00
|
|
|
if (!PathFileExistsA(server->ConfigPath) &&
|
2015-06-03 12:47:40 +03:00
|
|
|
!PathMakePathA(server->ConfigPath, 0))
|
2015-05-05 20:45:34 +03:00
|
|
|
{
|
|
|
|
WLog_ERR(TAG, "Failed to create directory '%s'", server->ConfigPath);
|
2015-06-17 23:08:02 +03:00
|
|
|
return FALSE;
|
2015-05-05 20:45:34 +03:00
|
|
|
}
|
2014-07-19 01:26:21 +04:00
|
|
|
|
2015-05-05 20:45:34 +03:00
|
|
|
if (!(filepath = GetCombinedPath(server->ConfigPath, "shadow")))
|
2015-06-17 23:08:02 +03:00
|
|
|
return FALSE;
|
2014-07-19 01:26:21 +04:00
|
|
|
|
2015-05-05 20:45:34 +03:00
|
|
|
if (!PathFileExistsA(filepath) &&
|
2015-06-03 12:47:40 +03:00
|
|
|
!PathMakePathA(filepath, 0))
|
2015-05-05 20:45:34 +03:00
|
|
|
{
|
2015-06-17 23:08:02 +03:00
|
|
|
if (!CreateDirectoryA(filepath, 0))
|
|
|
|
{
|
|
|
|
WLog_ERR(TAG, "Failed to create directory '%s'", filepath);
|
|
|
|
goto out_fail;
|
|
|
|
}
|
2015-05-05 20:45:34 +03:00
|
|
|
}
|
2014-07-19 01:26:21 +04:00
|
|
|
|
|
|
|
server->CertificateFile = GetCombinedPath(filepath, "shadow.crt");
|
|
|
|
server->PrivateKeyFile = GetCombinedPath(filepath, "shadow.key");
|
2015-06-17 23:08:02 +03:00
|
|
|
if (!server->CertificateFile || !server->PrivateKeyFile)
|
|
|
|
goto out_fail;
|
2014-07-19 01:26:21 +04:00
|
|
|
|
|
|
|
if ((!PathFileExistsA(server->CertificateFile)) ||
|
|
|
|
(!PathFileExistsA(server->PrivateKeyFile)))
|
|
|
|
{
|
|
|
|
makecert = makecert_context_new();
|
2015-06-17 23:08:02 +03:00
|
|
|
if (!makecert)
|
|
|
|
goto out_fail;
|
2014-07-19 01:26:21 +04:00
|
|
|
|
2015-06-28 20:39:09 +03:00
|
|
|
if (makecert_context_process(makecert, makecert_argc, (char**) makecert_argv) < 0)
|
2015-06-17 23:08:02 +03:00
|
|
|
goto out_fail;
|
2014-07-19 01:26:21 +04:00
|
|
|
|
2015-06-28 20:39:09 +03:00
|
|
|
if (makecert_context_set_output_file_name(makecert, "shadow") != 1)
|
2015-06-17 23:08:02 +03:00
|
|
|
goto out_fail;
|
2014-07-19 01:26:21 +04:00
|
|
|
|
|
|
|
if (!PathFileExistsA(server->CertificateFile))
|
2015-06-17 23:08:02 +03:00
|
|
|
{
|
|
|
|
if (makecert_context_output_certificate_file(makecert, filepath) != 1)
|
|
|
|
goto out_fail;
|
|
|
|
}
|
2014-07-19 01:26:21 +04:00
|
|
|
|
|
|
|
if (!PathFileExistsA(server->PrivateKeyFile))
|
2015-06-17 23:08:02 +03:00
|
|
|
{
|
|
|
|
if (makecert_context_output_private_key_file(makecert, filepath) != 1)
|
|
|
|
goto out_fail;
|
|
|
|
}
|
2014-07-19 01:26:21 +04:00
|
|
|
}
|
2015-06-17 23:08:02 +03:00
|
|
|
ret = TRUE;
|
|
|
|
out_fail:
|
|
|
|
makecert_context_free(makecert);
|
2014-07-19 01:26:21 +04:00
|
|
|
free(filepath);
|
|
|
|
|
2015-06-17 23:08:02 +03:00
|
|
|
return ret;
|
2014-07-19 01:26:21 +04:00
|
|
|
}
|
|
|
|
|
2014-07-14 21:33:20 +04:00
|
|
|
int shadow_server_init(rdpShadowServer* server)
|
2014-07-11 00:32:46 +04:00
|
|
|
{
|
2014-07-18 05:15:22 +04:00
|
|
|
int status;
|
|
|
|
|
2014-08-15 21:50:22 +04:00
|
|
|
winpr_InitializeSSL(WINPR_SSL_INIT_DEFAULT);
|
|
|
|
|
2014-07-15 02:01:29 +04:00
|
|
|
WTSRegisterWtsApiFunctionTable(FreeRDP_InitWtsApi());
|
|
|
|
|
2015-04-28 18:00:41 +03:00
|
|
|
if (!(server->clients = ArrayList_New(TRUE)))
|
|
|
|
goto fail_client_array;
|
2014-09-18 21:06:49 +04:00
|
|
|
|
2015-04-28 18:00:41 +03:00
|
|
|
if (!(server->StopEvent = CreateEvent(NULL, TRUE, FALSE, NULL)))
|
|
|
|
goto fail_stop_event;
|
2014-07-12 09:18:08 +04:00
|
|
|
|
2015-04-28 18:00:41 +03:00
|
|
|
if (!InitializeCriticalSectionAndSpinCount(&(server->lock), 4000))
|
|
|
|
goto fail_server_lock;
|
2014-09-18 21:06:49 +04:00
|
|
|
|
|
|
|
status = shadow_server_init_config_path(server);
|
|
|
|
|
|
|
|
if (status < 0)
|
2015-04-28 18:00:41 +03:00
|
|
|
goto fail_config_path;
|
2014-09-18 21:06:49 +04:00
|
|
|
|
2014-07-19 01:26:21 +04:00
|
|
|
status = shadow_server_init_certificate(server);
|
|
|
|
|
|
|
|
if (status < 0)
|
2015-04-28 18:00:41 +03:00
|
|
|
goto fail_certificate;
|
2014-07-19 01:26:21 +04:00
|
|
|
|
2014-07-12 03:30:40 +04:00
|
|
|
server->listener = freerdp_listener_new();
|
|
|
|
|
|
|
|
if (!server->listener)
|
2015-04-28 18:00:41 +03:00
|
|
|
goto fail_listener;
|
2014-07-12 03:30:40 +04:00
|
|
|
|
|
|
|
server->listener->info = (void*) server;
|
|
|
|
server->listener->PeerAccepted = shadow_client_accepted;
|
|
|
|
|
2015-04-08 20:30:57 +03:00
|
|
|
server->subsystem = shadow_subsystem_new();
|
2014-07-12 08:01:29 +04:00
|
|
|
|
|
|
|
if (!server->subsystem)
|
2015-04-28 18:00:41 +03:00
|
|
|
goto fail_subsystem_new;
|
2014-07-14 21:33:20 +04:00
|
|
|
|
2014-09-18 21:06:49 +04:00
|
|
|
status = shadow_subsystem_init(server->subsystem, server);
|
2014-07-18 05:15:22 +04:00
|
|
|
|
2015-04-28 18:00:41 +03:00
|
|
|
if (status >= 0)
|
|
|
|
return status;
|
|
|
|
|
2015-09-15 18:17:22 +03:00
|
|
|
shadow_subsystem_free(server->subsystem);
|
2015-04-28 18:00:41 +03:00
|
|
|
fail_subsystem_new:
|
|
|
|
freerdp_listener_free(server->listener);
|
|
|
|
server->listener = NULL;
|
|
|
|
fail_listener:
|
|
|
|
free(server->CertificateFile);
|
|
|
|
server->CertificateFile = NULL;
|
|
|
|
free(server->PrivateKeyFile);
|
|
|
|
server->PrivateKeyFile = NULL;
|
|
|
|
fail_certificate:
|
|
|
|
free(server->ConfigPath);
|
|
|
|
server->ConfigPath = NULL;
|
|
|
|
fail_config_path:
|
|
|
|
DeleteCriticalSection(&(server->lock));
|
|
|
|
fail_server_lock:
|
|
|
|
CloseHandle(server->StopEvent);
|
|
|
|
server->StopEvent = NULL;
|
|
|
|
fail_stop_event:
|
|
|
|
ArrayList_Free(server->clients);
|
|
|
|
server->clients = NULL;
|
|
|
|
fail_client_array:
|
|
|
|
WLog_ERR(TAG, "Failed to initialize shadow server");
|
|
|
|
return -1;
|
2014-07-14 21:33:20 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
int shadow_server_uninit(rdpShadowServer* server)
|
|
|
|
{
|
2015-09-15 18:17:22 +03:00
|
|
|
if (!server)
|
|
|
|
return -1;
|
|
|
|
|
2014-07-14 21:33:20 +04:00
|
|
|
shadow_server_stop(server);
|
|
|
|
|
2015-09-15 18:17:22 +03:00
|
|
|
shadow_subsystem_uninit(server->subsystem);
|
2014-07-14 21:33:20 +04:00
|
|
|
|
2015-09-15 18:17:22 +03:00
|
|
|
shadow_subsystem_free(server->subsystem);
|
2014-07-19 01:26:21 +04:00
|
|
|
|
2015-09-15 18:17:22 +03:00
|
|
|
freerdp_listener_free(server->listener);
|
|
|
|
server->listener = NULL;
|
2014-08-06 20:08:00 +04:00
|
|
|
|
2015-09-15 18:17:22 +03:00
|
|
|
free(server->CertificateFile);
|
|
|
|
server->CertificateFile = NULL;
|
|
|
|
free(server->PrivateKeyFile);
|
|
|
|
server->PrivateKeyFile = NULL;
|
|
|
|
|
|
|
|
free(server->ConfigPath);
|
|
|
|
server->ConfigPath = NULL;
|
|
|
|
|
|
|
|
DeleteCriticalSection(&(server->lock));
|
|
|
|
|
|
|
|
CloseHandle(server->StopEvent);
|
|
|
|
server->StopEvent = NULL;
|
|
|
|
|
|
|
|
ArrayList_Free(server->clients);
|
|
|
|
server->clients = NULL;
|
2014-09-18 21:06:49 +04:00
|
|
|
|
2014-07-14 21:33:20 +04:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
rdpShadowServer* shadow_server_new()
|
|
|
|
{
|
|
|
|
rdpShadowServer* server;
|
|
|
|
|
|
|
|
server = (rdpShadowServer*) calloc(1, sizeof(rdpShadowServer));
|
|
|
|
|
|
|
|
if (!server)
|
2014-07-12 04:49:56 +04:00
|
|
|
return NULL;
|
|
|
|
|
2014-07-14 21:33:20 +04:00
|
|
|
server->port = 3389;
|
2014-07-15 02:01:29 +04:00
|
|
|
server->mayView = TRUE;
|
|
|
|
server->mayInteract = TRUE;
|
2014-07-14 21:33:20 +04:00
|
|
|
|
2016-06-02 21:33:31 +03:00
|
|
|
server->rfxMode = RLGR3;
|
|
|
|
server->h264RateControlMode = H264_RATECONTROL_VBR;
|
|
|
|
server->h264BitRate = 1000000;
|
|
|
|
server->h264FrameRate = 30;
|
|
|
|
server->h264QP = 0;
|
|
|
|
|
2014-09-27 03:03:48 +04:00
|
|
|
server->authentication = FALSE;
|
|
|
|
|
2016-07-22 00:53:20 +03:00
|
|
|
server->settings = freerdp_settings_new(FREERDP_SETTINGS_SERVER_MODE);
|
|
|
|
|
2014-07-11 00:32:46 +04:00
|
|
|
return server;
|
|
|
|
}
|
|
|
|
|
2014-07-11 01:20:41 +04:00
|
|
|
void shadow_server_free(rdpShadowServer* server)
|
2014-07-11 00:32:46 +04:00
|
|
|
{
|
2014-07-12 03:30:40 +04:00
|
|
|
if (!server)
|
|
|
|
return;
|
|
|
|
|
2016-07-22 16:06:07 +03:00
|
|
|
free(server->ipcSocket);
|
|
|
|
server->ipcSocket = NULL;
|
2016-07-22 00:53:20 +03:00
|
|
|
|
2016-07-22 16:06:07 +03:00
|
|
|
freerdp_settings_free(server->settings);
|
|
|
|
server->settings = NULL;
|
2014-07-12 03:30:40 +04:00
|
|
|
|
|
|
|
free(server);
|
2014-07-11 00:32:46 +04:00
|
|
|
}
|
|
|
|
|