diff --git a/server/proxy/modules/capture/CMakeLists.txt b/server/proxy/modules/capture/CMakeLists.txt new file mode 100644 index 000000000..80ba3b77e --- /dev/null +++ b/server/proxy/modules/capture/CMakeLists.txt @@ -0,0 +1,33 @@ +# +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP Proxy Server Capture Module +# +# Copyright 2019 Kobi Mizrachi +# +# 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. +# + +set(PLUGIN_NAME "proxy-capture-plugin") + +add_library(${PLUGIN_NAME} MODULE + cap_main.c + cap_config.c + cap_config.h + cap_protocol.c + cap_protocol.h +) + +set_target_properties(${PLUGIN_NAME} PROPERTIES PREFIX "") +set_target_properties(${PLUGIN_NAME} PROPERTIES NO_SONAME 1) +set_target_properties(${PLUGIN_NAME} PROPERTIES +LIBRARY_OUTPUT_DIRECTORY "${FREERDP_PROXY_PLUGINDIR}") diff --git a/server/proxy/modules/capture/cap_config.c b/server/proxy/modules/capture/cap_config.c new file mode 100644 index 000000000..ae4b6a647 --- /dev/null +++ b/server/proxy/modules/capture/cap_config.c @@ -0,0 +1,97 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Proxy Server Session Capture Module + * + * Copyright 2019 Kobi Mizrachi + * + * 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. + */ + +#include +#include +#include + +#include "cap_config.h" + +BOOL capture_plugin_init_config(captureConfig* config) +{ + const char* name = "PROXY_CAPTURE_TARGET"; + char* tmp = NULL; + DWORD nSize = GetEnvironmentVariableA(name, NULL, 0); + + if (nSize) + { + char* colon; + int addrLen; + unsigned long port; + + tmp = (LPSTR)malloc(nSize); + if (!tmp) + return FALSE; + + if (GetEnvironmentVariableA(name, tmp, nSize) != nSize - 1) + { + free(tmp); + return FALSE; + } + + colon = strchr(tmp, ':'); + + if (!colon) + { + free(tmp); + return FALSE; + } + + addrLen = (int)(colon - tmp); + config->host = malloc(addrLen + 1); + if (!config->host) + { + free(tmp); + return FALSE; + } + + strncpy(config->host, tmp, addrLen); + config->host[addrLen] = '\0'; + + port = strtoul(colon + 1, NULL, 0); + + if ((errno != 0) || (port > UINT16_MAX)) + { + free(config->host); + config->host = NULL; + + free(tmp); + return FALSE; + } + + config->port = port; + free(tmp); + } + else + { + config->host = _strdup("127.0.0.1"); + if (!config->host) + return FALSE; + + config->port = 8889; + } + + return TRUE; +} + +void capture_plugin_config_free_internal(captureConfig* config) +{ + free(config->host); + config->host = NULL; +} diff --git a/server/proxy/modules/capture/cap_config.h b/server/proxy/modules/capture/cap_config.h new file mode 100644 index 000000000..ace5079dc --- /dev/null +++ b/server/proxy/modules/capture/cap_config.h @@ -0,0 +1,29 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Proxy Server Session Capture Module + * + * Copyright 2019 Kobi Mizrachi + * + * 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. + */ + +#include + +typedef struct capture_config +{ + UINT16 port; + char* host; +} captureConfig; + +BOOL capture_plugin_init_config(captureConfig* config); +void capture_plugin_config_free_internal(captureConfig* config); diff --git a/server/proxy/modules/capture/cap_main.c b/server/proxy/modules/capture/cap_main.c new file mode 100644 index 000000000..8780900ae --- /dev/null +++ b/server/proxy/modules/capture/cap_main.c @@ -0,0 +1,287 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Proxy Server Session Capture Module + * + * Copyright 2019 Kobi Mizrachi + * + * 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. + */ + +#define TAG MODULE_TAG("capture") + +#define PLUGIN_NAME "capture" +#define PLUGIN_DESC "stream egfx connections over tcp" + +#include +#include +#include +#include + +#include "pf_log.h" +#include "modules_api.h" +#include "pf_context.h" +#include "cap_config.h" +#include "cap_protocol.h" + +#define BUFSIZE 8092 + +static proxyPluginsManager* g_plugins_manager = NULL; +static captureConfig config = { 0 }; + +static SOCKET capture_plugin_init_socket() +{ + int status; + int sockfd; + struct sockaddr_in addr = { 0 }; + sockfd = _socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + + if (sockfd == -1) + return -1; + + addr.sin_family = AF_INET; + addr.sin_port = htons(config.port); + inet_pton(AF_INET, config.host, &(addr.sin_addr)); + + status = _connect(sockfd, (const struct sockaddr*)&addr, sizeof(addr)); + if (status < 0) + { + close(sockfd); + return -1; + } + + return sockfd; +} + +static BOOL capture_plugin_send_data(SOCKET sockfd, const BYTE* buffer, size_t len) +{ + size_t chunk_len; + int nsent; + + if (!buffer) + return FALSE; + + while (len > 0) + { + chunk_len = len > BUFSIZE ? BUFSIZE : len; + nsent = _send(sockfd, (const char*)buffer, chunk_len, 0); + if (nsent == -1) + return FALSE; + + buffer += nsent; + len -= nsent; + } + + return TRUE; +} + +static BOOL capture_plugin_send_packet(SOCKET sockfd, wStream* packet) +{ + size_t len; + BYTE* buffer; + BOOL result = FALSE; + + if (!packet) + return FALSE; + + buffer = Stream_Buffer(packet); + len = Stream_Capacity(packet); + + if (!capture_plugin_send_data(sockfd, buffer, len)) + { + WLog_ERR(TAG, "error while transmitting frame: errno=%d", errno); + goto error; + } + + result = TRUE; + +error: + Stream_Free(packet, TRUE); + return result; +} + +static SOCKET capture_plugin_get_socket(proxyData* pdata) +{ + void* custom; + + custom = g_plugins_manager->GetPluginData(PLUGIN_NAME, pdata); + if (!custom) + return -1; + + return (SOCKET)custom; +} + +static BOOL capture_plugin_session_end(proxyData* pdata) +{ + SOCKET socket; + BOOL ret; + + socket = capture_plugin_get_socket(pdata); + if (socket == -1) + return FALSE; + + wStream* s = capture_plugin_packet_new(SESSION_END_PDU_BASE_SIZE, MESSAGE_TYPE_SESSION_END); + if (!s) + return FALSE; + + ret = capture_plugin_send_packet(socket, s); + + closesocket(socket); + return ret; +} + +static BOOL capture_plugin_send_frame(pClientContext* pc, SOCKET socket, const BYTE* buffer) +{ + size_t frame_size; + BOOL ret = FALSE; + wStream* s = NULL; + BYTE* bmp_header = NULL; + rdpSettings* settings = pc->context.settings; + + frame_size = settings->DesktopWidth * settings->DesktopHeight * (settings->ColorDepth / 8); + bmp_header = winpr_bitmap_construct_header(settings->DesktopWidth, settings->DesktopHeight, + settings->ColorDepth); + + if (!bmp_header) + return FALSE; + + /* + * capture frame packet indicates a packet that contains a frame buffer. payload length is + * marked as 0, and receiving side must read `frame_size` bytes, a constant size of + * width*height*(bpp/8) from the socket, to receive the full frame buffer. + */ + s = capture_plugin_packet_new(0, MESSAGE_TYPE_CAPTURED_FRAME); + if (!s) + goto error; + + if (!capture_plugin_send_packet(socket, s)) + goto error; + + ret = capture_plugin_send_data(socket, bmp_header, WINPR_IMAGE_BMP_HEADER_LEN); + if (!ret) + goto error; + + ret = capture_plugin_send_data(socket, buffer, frame_size); + +error: + free(bmp_header); + return ret; +} + +static BOOL capture_plugin_client_end_paint(proxyData* pdata) +{ + pClientContext* pc = pdata->pc; + rdpGdi* gdi = pc->context.gdi; + SOCKET socket; + + if (gdi->suppressOutput) + return TRUE; + + if (gdi->primary->hdc->hwnd->ninvalid < 1) + return TRUE; + + socket = capture_plugin_get_socket(pdata); + if (socket == -1) + return FALSE; + + if (!capture_plugin_send_frame(pc, socket, gdi->primary_buffer)) + { + WLog_ERR(TAG, "capture_plugin_send_frame failed!"); + return FALSE; + } + + gdi->primary->hdc->hwnd->invalid->null = TRUE; + gdi->primary->hdc->hwnd->ninvalid = 0; + return TRUE; +} + +static BOOL capture_plugin_client_post_connect(proxyData* pdata) +{ + SOCKET socket; + wStream* s; + pClientContext* pc = pdata->pc; + rdpSettings* settings = pc->context.settings; + + socket = capture_plugin_init_socket(); + if (socket == -1) + { + WLog_ERR(TAG, "failed to establish a connection"); + return FALSE; + } + + g_plugins_manager->SetPluginData(PLUGIN_NAME, pdata, (void*)socket); + + s = capture_plugin_create_session_info_packet(settings); + if (!s) + return FALSE; + + return capture_plugin_send_packet(socket, s); +} + +static BOOL capture_plugin_server_post_connect(proxyData* pdata) +{ + pServerContext* ps = pdata->ps; + proxyConfig* config = pdata->config; + rdpSettings* settings = ps->context.settings; + + if (!config->GFX || !config->SessionCapture) + { + WLog_ERR(TAG, "config options 'GFX' and 'SessionCapture' options must be set to true!"); + return FALSE; + } + + if (!settings->SupportGraphicsPipeline) + { + WLog_ERR(TAG, "session capture is only supported for GFX clients, denying connection"); + return FALSE; + } + + return TRUE; +} + +static BOOL capture_plugin_unload() +{ + capture_plugin_config_free_internal(&config); + return TRUE; +} + +static proxyPlugin demo_plugin = { + PLUGIN_NAME, /* name */ + PLUGIN_DESC, /* description */ + capture_plugin_unload, /* PluginUnload */ + NULL, /* ClientPreConnect */ + capture_plugin_client_post_connect, /* ClientPostConnect */ + NULL, /* ClientLoginFailure */ + capture_plugin_client_end_paint, /* ClientEndPaint */ + capture_plugin_server_post_connect, /* ServerPostConnect */ + NULL, /* ServerChannelsInit */ + NULL, /* ServerChannelsFree */ + capture_plugin_session_end, /* Session End */ + NULL, /* KeyboardEvent */ + NULL, /* MouseEvent */ + NULL, /* ClientChannelData */ + NULL, /* ServerChannelData */ +}; + +BOOL proxy_module_entry_point(proxyPluginsManager* plugins_manager) +{ + g_plugins_manager = plugins_manager; + + if (!capture_plugin_init_config(&config)) + { + WLog_ERR(TAG, "failed to load config"); + return FALSE; + } + + WLog_INFO(TAG, "host: %s, port: %" PRIu16 "", config.host, config.port); + return plugins_manager->RegisterPlugin(&demo_plugin); +} diff --git a/server/proxy/modules/capture/cap_protocol.c b/server/proxy/modules/capture/cap_protocol.c new file mode 100644 index 000000000..b56393838 --- /dev/null +++ b/server/proxy/modules/capture/cap_protocol.c @@ -0,0 +1,57 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Proxy Server Session Capture Module + * + * Copyright 2019 Kobi Mizrachi + * + * 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. + */ + +#include "cap_protocol.h" + +wStream* capture_plugin_packet_new(UINT32 payload_size, UINT16 type) +{ + wStream* stream = Stream_New(NULL, HEADER_SIZE + payload_size); + + if (!stream) + return NULL; + + Stream_Write_UINT32(stream, payload_size); + Stream_Write_UINT16(stream, type); + return stream; +} + +wStream* capture_plugin_create_session_info_packet(rdpSettings* settings) +{ + UINT16 username_length; + wStream* s = NULL; + + if (!settings || !settings->Username) + return NULL; + + username_length = strlen(settings->Username); + if (username_length == 0) + return NULL; + + s = capture_plugin_packet_new(SESSION_INFO_PDU_BASE_SIZE + username_length, + MESSAGE_TYPE_SESSION_INFO); + if (!s) + return NULL; + + Stream_Write_UINT16(s, username_length); /* username length (2 bytes) */ + Stream_Write(s, settings->Username, username_length); /* username */ + Stream_Write_UINT32(s, settings->DesktopWidth); /* desktop width (4 bytes) */ + Stream_Write_UINT32(s, settings->DesktopHeight); /* desktop height (4 bytes) */ + Stream_Write_UINT32(s, settings->ColorDepth); /* color depth (4 bytes) */ + return s; +} diff --git a/server/proxy/modules/capture/cap_protocol.h b/server/proxy/modules/capture/cap_protocol.h new file mode 100644 index 000000000..655eb69bd --- /dev/null +++ b/server/proxy/modules/capture/cap_protocol.h @@ -0,0 +1,35 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Proxy Server Session Capture Module + * + * Copyright 2019 Kobi Mizrachi + * + * 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. + */ + +#include +#include + +/* protocol message sizes */ +#define HEADER_SIZE 6 +#define SESSION_INFO_PDU_BASE_SIZE 14 +#define SESSION_END_PDU_BASE_SIZE 0 +#define CAPTURED_FRAME_PDU_BASE_SIZE 0 + +/* protocol message types */ +#define MESSAGE_TYPE_SESSION_INFO 1 +#define MESSAGE_TYPE_CAPTURED_FRAME 2 +#define MESSAGE_TYPE_SESSION_END 3 + +wStream* capture_plugin_packet_new(UINT32 payload_size, UINT16 type); +wStream* capture_plugin_create_session_info_packet(rdpSettings* settings);