From 44c1ec3276f520f09ead43ce0bf727fe971613f3 Mon Sep 17 00:00:00 2001 From: Michael Saxl Date: Sat, 17 Jun 2023 21:59:49 +0200 Subject: [PATCH] [gateway,websocket] implement plain websocket transport * factor out most websocket specific code parts into websocket.c * create wst.c (Websocket Transport) as gateway transport implementation * introduce GatewayUrl setting that holds the websocket url * introduce GatewayHttpExtAuthBearer that holds the HTTP Bearer * GatewayHttpExtAuthBearer can be used by both rdg and wst --- client/common/cmdline.c | 13 +- client/common/cmdline.h | 2 +- include/freerdp/settings.h | 4 +- libfreerdp/common/settings_getters.c | 36 +- libfreerdp/common/settings_str.c | 5 +- libfreerdp/core/CMakeLists.txt | 4 + libfreerdp/core/gateway/rdg.c | 449 +--------- libfreerdp/core/gateway/rdg.h | 13 +- libfreerdp/core/gateway/websocket.c | 521 ++++++++++++ libfreerdp/core/gateway/websocket.h | 71 ++ libfreerdp/core/gateway/wst.c | 773 ++++++++++++++++++ libfreerdp/core/gateway/wst.h | 39 + .../core/test/settings_property_lists.h | 2 + libfreerdp/core/transport.c | 40 + libfreerdp/core/transport.h | 1 - 15 files changed, 1499 insertions(+), 474 deletions(-) create mode 100644 libfreerdp/core/gateway/websocket.c create mode 100644 libfreerdp/core/gateway/websocket.h create mode 100644 libfreerdp/core/gateway/wst.c create mode 100644 libfreerdp/core/gateway/wst.h diff --git a/client/common/cmdline.c b/client/common/cmdline.c index fdb346847..5ae1998fa 100644 --- a/client/common/cmdline.c +++ b/client/common/cmdline.c @@ -2540,7 +2540,7 @@ static BOOL parse_gateway_options(rdpSettings* settings, const COMMAND_LINE_ARGU validOption = TRUE; allowHttpOpts = FALSE; } - + const char* bearer = option_starts_with("bearer:", argval); if (bearer) { @@ -2550,6 +2550,17 @@ static BOOL parse_gateway_options(rdpSettings* settings, const COMMAND_LINE_ARGU allowHttpOpts = FALSE; } + const char* gwurl = option_starts_with("url:", argval); + if (gwurl) + { + if (!freerdp_settings_set_string(settings, FreeRDP_GatewayUrl, gwurl)) + goto fail; + if (!freerdp_set_gateway_usage_method(settings, TSC_PROXY_MODE_DIRECT)) + goto fail; + validOption = TRUE; + allowHttpOpts = FALSE; + } + const char* um = option_starts_with("usage-method:", argval); if (um) { diff --git a/client/common/cmdline.h b/client/common/cmdline.h index 6e5f7f016..9ac06463d 100644 --- a/client/common/cmdline.h +++ b/client/common/cmdline.h @@ -177,7 +177,7 @@ static const COMMAND_LINE_ARGUMENT_A global_cmd_args[] = { "g:[:],u:,d:,p:,usage-method:[" "direct|detect],access-token:<" "token>,type:[rpc|http[,no-websockets][,extauth-sspi-ntlm]|auto[,no-websockets][,extauth-" - "sspi-ntlm]],", + "sspi-ntlm]],url:,bearer:", NULL, NULL, -1, "gw", "Gateway Hostname" }, #if defined(WITH_FREERDP_DEPRECATED_COMMANDLINE) { "g", COMMAND_LINE_VALUE_REQUIRED, "[:]", NULL, NULL, -1, NULL, diff --git a/include/freerdp/settings.h b/include/freerdp/settings.h index 8691b8759..b136b37da 100644 --- a/include/freerdp/settings.h +++ b/include/freerdp/settings.h @@ -767,6 +767,7 @@ extern "C" #define FreeRDP_GatewayHttpUseWebsockets (2000) #define FreeRDP_GatewayHttpExtAuthSspiNtlm (2001) #define FreeRDP_GatewayHttpExtAuthBearer (2002) +#define FreeRDP_GatewayUrl (2003) #define FreeRDP_ProxyType (2015) #define FreeRDP_ProxyHostname (2016) #define FreeRDP_ProxyPort (2017) @@ -1342,7 +1343,8 @@ extern "C" ALIGN64 BOOL GatewayHttpUseWebsockets; /* 2000 */ ALIGN64 BOOL GatewayHttpExtAuthSspiNtlm; /* 2001 */ ALIGN64 char* GatewayHttpExtAuthBearer; /* 2002 */ - UINT64 padding2015[2015 - 2003]; /* 2003 */ + ALIGN64 char* GatewayUrl; /* 2003 */ + UINT64 padding2015[2015 - 2004]; /* 2004 */ /* Proxy */ ALIGN64 UINT32 ProxyType; /* 2015 */ diff --git a/libfreerdp/common/settings_getters.c b/libfreerdp/common/settings_getters.c index ad8eafbf5..aaaf4d011 100644 --- a/libfreerdp/common/settings_getters.c +++ b/libfreerdp/common/settings_getters.c @@ -2636,15 +2636,18 @@ const char* freerdp_settings_get_string(const rdpSettings* settings, size_t id) case FreeRDP_GatewayHostname: return settings->GatewayHostname; + case FreeRDP_GatewayHttpExtAuthBearer: + return settings->GatewayHttpExtAuthBearer; + case FreeRDP_GatewayPassword: return settings->GatewayPassword; + case FreeRDP_GatewayUrl: + return settings->GatewayUrl; + case FreeRDP_GatewayUsername: return settings->GatewayUsername; - case FreeRDP_GatewayHttpExtAuthBearer: - return settings->GatewayHttpExtAuthBearer; - case FreeRDP_HomePath: return settings->HomePath; @@ -2908,15 +2911,18 @@ char* freerdp_settings_get_string_writable(rdpSettings* settings, size_t id) case FreeRDP_GatewayHostname: return settings->GatewayHostname; + case FreeRDP_GatewayHttpExtAuthBearer: + return settings->GatewayHttpExtAuthBearer; + case FreeRDP_GatewayPassword: return settings->GatewayPassword; + case FreeRDP_GatewayUrl: + return settings->GatewayUrl; + case FreeRDP_GatewayUsername: return settings->GatewayUsername; - case FreeRDP_GatewayHttpExtAuthBearer: - return settings->GatewayHttpExtAuthBearer; - case FreeRDP_HomePath: return settings->HomePath; @@ -3189,15 +3195,18 @@ BOOL freerdp_settings_set_string_(rdpSettings* settings, size_t id, char* val, s case FreeRDP_GatewayHostname: return update_string_(&settings->GatewayHostname, cnv.c, len); + case FreeRDP_GatewayHttpExtAuthBearer: + return update_string_(&settings->GatewayHttpExtAuthBearer, cnv.c, len); + case FreeRDP_GatewayPassword: return update_string_(&settings->GatewayPassword, cnv.c, len); + case FreeRDP_GatewayUrl: + return update_string_(&settings->GatewayUrl, cnv.c, len); + case FreeRDP_GatewayUsername: return update_string_(&settings->GatewayUsername, cnv.c, len); - case FreeRDP_GatewayHttpExtAuthBearer: - return update_string_(&settings->GatewayHttpExtAuthBearer, cnv.c, len); - case FreeRDP_HomePath: return update_string_(&settings->HomePath, cnv.c, len); @@ -3486,15 +3495,18 @@ BOOL freerdp_settings_set_string_copy_(rdpSettings* settings, size_t id, const c case FreeRDP_GatewayHostname: return update_string_copy_(&settings->GatewayHostname, cnv.cc, len, cleanup); + case FreeRDP_GatewayHttpExtAuthBearer: + return update_string_copy_(&settings->GatewayHttpExtAuthBearer, cnv.cc, len, cleanup); + case FreeRDP_GatewayPassword: return update_string_copy_(&settings->GatewayPassword, cnv.cc, len, cleanup); + case FreeRDP_GatewayUrl: + return update_string_copy_(&settings->GatewayUrl, cnv.cc, len, cleanup); + case FreeRDP_GatewayUsername: return update_string_copy_(&settings->GatewayUsername, cnv.cc, len, cleanup); - case FreeRDP_GatewayHttpExtAuthBearer: - return update_string_copy_(&settings->GatewayHttpExtAuthBearer, cnv.cc, len, cleanup); - case FreeRDP_HomePath: return update_string_copy_(&settings->HomePath, cnv.cc, len, cleanup); diff --git a/libfreerdp/common/settings_str.c b/libfreerdp/common/settings_str.c index c76aa79cf..effe7c41b 100644 --- a/libfreerdp/common/settings_str.c +++ b/libfreerdp/common/settings_str.c @@ -106,8 +106,6 @@ static const struct settings_str_entry settings_map[] = { { FreeRDP_GatewayEnabled, FREERDP_SETTINGS_TYPE_BOOL, "FreeRDP_GatewayEnabled" }, { FreeRDP_GatewayHttpExtAuthSspiNtlm, FREERDP_SETTINGS_TYPE_BOOL, "FreeRDP_GatewayHttpExtAuthSspiNtlm" }, - { FreeRDP_GatewayHttpExtAuthBearer, FREERDP_SETTINGS_TYPE_STRING, - "FreeRDP_GatewayHttpExtAuthBearer" }, { FreeRDP_GatewayHttpTransport, FREERDP_SETTINGS_TYPE_BOOL, "FreeRDP_GatewayHttpTransport" }, { FreeRDP_GatewayHttpUseWebsockets, FREERDP_SETTINGS_TYPE_BOOL, "FreeRDP_GatewayHttpUseWebsockets" }, @@ -460,7 +458,10 @@ static const struct settings_str_entry settings_map[] = { { FreeRDP_GatewayAccessToken, FREERDP_SETTINGS_TYPE_STRING, "FreeRDP_GatewayAccessToken" }, { FreeRDP_GatewayDomain, FREERDP_SETTINGS_TYPE_STRING, "FreeRDP_GatewayDomain" }, { FreeRDP_GatewayHostname, FREERDP_SETTINGS_TYPE_STRING, "FreeRDP_GatewayHostname" }, + { FreeRDP_GatewayHttpExtAuthBearer, FREERDP_SETTINGS_TYPE_STRING, + "FreeRDP_GatewayHttpExtAuthBearer" }, { FreeRDP_GatewayPassword, FREERDP_SETTINGS_TYPE_STRING, "FreeRDP_GatewayPassword" }, + { FreeRDP_GatewayUrl, FREERDP_SETTINGS_TYPE_STRING, "FreeRDP_GatewayUrl" }, { FreeRDP_GatewayUsername, FREERDP_SETTINGS_TYPE_STRING, "FreeRDP_GatewayUsername" }, { FreeRDP_HomePath, FREERDP_SETTINGS_TYPE_STRING, "FreeRDP_HomePath" }, { FreeRDP_ImeFileName, FREERDP_SETTINGS_TYPE_STRING, "FreeRDP_ImeFileName" }, diff --git a/libfreerdp/core/CMakeLists.txt b/libfreerdp/core/CMakeLists.txt index 43a5e4690..9b426816e 100644 --- a/libfreerdp/core/CMakeLists.txt +++ b/libfreerdp/core/CMakeLists.txt @@ -43,6 +43,10 @@ set(${MODULE_PREFIX}_GATEWAY_SRCS ${${MODULE_PREFIX}_GATEWAY_DIR}/rts_signature.h ${${MODULE_PREFIX}_GATEWAY_DIR}/http.c ${${MODULE_PREFIX}_GATEWAY_DIR}/http.h + ${${MODULE_PREFIX}_GATEWAY_DIR}/websocket.c + ${${MODULE_PREFIX}_GATEWAY_DIR}/websocket.h + ${${MODULE_PREFIX}_GATEWAY_DIR}/wst.c + ${${MODULE_PREFIX}_GATEWAY_DIR}/wst.h ${${MODULE_PREFIX}_GATEWAY_DIR}/ncacn_http.c ${${MODULE_PREFIX}_GATEWAY_DIR}/ncacn_http.h) diff --git a/libfreerdp/core/gateway/rdg.c b/libfreerdp/core/gateway/rdg.c index 67ff4a349..db6ac80d7 100644 --- a/libfreerdp/core/gateway/rdg.c +++ b/libfreerdp/core/gateway/rdg.c @@ -34,6 +34,7 @@ #include #include "rdg.h" +#include "websocket.h" #include "../credssp_auth.h" #include "../proxy.h" #include "../rdp.h" @@ -110,42 +111,6 @@ #define HTTP_CAPABILITY_REAUTH 0x10 #define HTTP_CAPABILITY_UDP_TRANSPORT 0x20 -#define WEBSOCKET_MASK_BIT 0x80 -#define WEBSOCKET_FIN_BIT 0x80 - -typedef enum -{ - WebsocketContinuationOpcode = 0x0, - WebsocketTextOpcode = 0x1, - WebsocketBinaryOpcode = 0x2, - WebsocketCloseOpcode = 0x8, - WebsocketPingOpcode = 0x9, - WebsocketPongOpcode = 0xa, -} WEBSOCKET_OPCODE; - -typedef enum -{ - WebsocketStateOpcodeAndFin, - WebsocketStateLengthAndMasking, - WebsocketStateShortLength, - WebsocketStateLongLength, - WebSocketStateMaskingKey, - WebSocketStatePayload, -} WEBSOCKET_STATE; - -typedef struct -{ - size_t payloadLength; - uint32_t maskingKey; - BOOL masking; - BOOL closeSent; - BYTE opcode; - BYTE fragmentOriginalOpcode; - BYTE lengthAndMaskPosition; - WEBSOCKET_STATE state; - wStream* responseStreamBuffer; -} rdg_http_websocket_context; - typedef enum { ChunkStateLenghHeader, @@ -168,7 +133,7 @@ typedef struct union _context { rdg_http_encoding_chunked_context chunked; - rdg_http_websocket_context websocket; + websocket_context websocket; } context; } rdg_http_encoding_context; @@ -378,424 +343,18 @@ static BOOL rdg_write_chunked(BIO* bio, wStream* sPacket) return TRUE; } -static BOOL rdg_write_websocket(BIO* bio, wStream* sPacket, WEBSOCKET_OPCODE opcode) -{ - size_t len; - size_t fullLen; - int status; - wStream* sWS; - - uint32_t maskingKey; - - size_t streamPos; - - len = Stream_Length(sPacket); - Stream_SetPosition(sPacket, 0); - - if (len > INT_MAX) - return FALSE; - - if (len < 126) - fullLen = len + 6; /* 2 byte "mini header" + 4 byte masking key */ - else if (len < 0x10000) - fullLen = len + 8; /* 2 byte "mini header" + 2 byte length + 4 byte masking key */ - else - fullLen = len + 14; /* 2 byte "mini header" + 8 byte length + 4 byte masking key */ - - sWS = Stream_New(NULL, fullLen); - if (!sWS) - return FALSE; - - winpr_RAND(&maskingKey, 4); - - Stream_Write_UINT8(sWS, WEBSOCKET_FIN_BIT | opcode); - if (len < 126) - Stream_Write_UINT8(sWS, len | WEBSOCKET_MASK_BIT); - else if (len < 0x10000) - { - Stream_Write_UINT8(sWS, 126 | WEBSOCKET_MASK_BIT); - Stream_Write_UINT16_BE(sWS, len); - } - else - { - Stream_Write_UINT8(sWS, 127 | WEBSOCKET_MASK_BIT); - Stream_Write_UINT32_BE(sWS, 0); /* payload is limited to INT_MAX */ - Stream_Write_UINT32_BE(sWS, len); - } - Stream_Write_UINT32(sWS, maskingKey); - - /* mask as much as possible with 32bit access */ - for (streamPos = 0; streamPos + 4 <= len; streamPos += 4) - { - uint32_t data; - Stream_Read_UINT32(sPacket, data); - Stream_Write_UINT32(sWS, data ^ maskingKey); - } - - /* mask the rest byte by byte */ - for (; streamPos < len; streamPos++) - { - BYTE data; - BYTE* partialMask = ((BYTE*)&maskingKey) + (streamPos % 4); - Stream_Read_UINT8(sPacket, data); - Stream_Write_UINT8(sWS, data ^ *partialMask); - } - - Stream_SealLength(sWS); - - ERR_clear_error(); - status = BIO_write(bio, Stream_Buffer(sWS), Stream_Length(sWS)); - Stream_Free(sWS, TRUE); - - if (status != (SSIZE_T)fullLen) - return FALSE; - - return TRUE; -} - static BOOL rdg_write_packet(rdpRdg* rdg, wStream* sPacket) { if (rdg->transferEncoding.isWebsocketTransport) { if (rdg->transferEncoding.context.websocket.closeSent) return FALSE; - return rdg_write_websocket(rdg->tlsOut->bio, sPacket, WebsocketBinaryOpcode); + return websocket_write_wstream(rdg->tlsOut->bio, sPacket, WebsocketBinaryOpcode); } return rdg_write_chunked(rdg->tlsIn->bio, sPacket); } -static int rdg_websocket_read_data(BIO* bio, BYTE* pBuffer, size_t size, - rdg_http_websocket_context* encodingContext) -{ - int status; - - if (encodingContext->payloadLength == 0) - { - encodingContext->state = WebsocketStateOpcodeAndFin; - return 0; - } - - ERR_clear_error(); - status = - BIO_read(bio, pBuffer, - (encodingContext->payloadLength < size ? encodingContext->payloadLength : size)); - if (status <= 0) - return status; - - encodingContext->payloadLength -= status; - - if (encodingContext->payloadLength == 0) - encodingContext->state = WebsocketStateOpcodeAndFin; - - return status; -} - -static int rdg_websocket_read_discard(BIO* bio, rdg_http_websocket_context* encodingContext) -{ - char _dummy[256]; - int status; - - if (encodingContext->payloadLength == 0) - { - encodingContext->state = WebsocketStateOpcodeAndFin; - return 0; - } - - ERR_clear_error(); - status = BIO_read(bio, _dummy, sizeof(_dummy)); - if (status <= 0) - return status; - - encodingContext->payloadLength -= status; - - if (encodingContext->payloadLength == 0) - encodingContext->state = WebsocketStateOpcodeAndFin; - - return status; -} - -static int rdg_websocket_read_wstream(BIO* bio, wStream* s, - rdg_http_websocket_context* encodingContext) -{ - int status; - - if (encodingContext->payloadLength == 0) - { - encodingContext->state = WebsocketStateOpcodeAndFin; - return 0; - } - if (s == NULL) - { - WLog_WARN(TAG, "wStream* s=%p", s); - return -1; - } - if (Stream_GetRemainingCapacity(s) != encodingContext->payloadLength) - { - WLog_WARN(TAG, - "wStream::capacity [%" PRIuz "] != encodingContext::paylaodLangth [%" PRIuz "]", - Stream_GetRemainingCapacity(s), encodingContext->payloadLength); - return -1; - } - - ERR_clear_error(); - status = BIO_read(bio, Stream_Pointer(s), encodingContext->payloadLength); - if (status <= 0) - return status; - - Stream_Seek(s, status); - - encodingContext->payloadLength -= status; - - if (encodingContext->payloadLength == 0) - { - encodingContext->state = WebsocketStateOpcodeAndFin; - Stream_SealLength(s); - Stream_SetPosition(s, 0); - } - - return status; -} - -static BOOL rdg_websocket_reply_close(BIO* bio, wStream* s) -{ - /* write back close */ - wStream* closeFrame; - uint16_t maskingKey1; - uint16_t maskingKey2; - int status; - size_t closeDataLen; - - closeDataLen = 0; - if (s != NULL && Stream_Length(s) >= 2) - closeDataLen = 2; - - closeFrame = Stream_New(NULL, 6 + closeDataLen); - if (!closeFrame) - return FALSE; - - Stream_Write_UINT8(closeFrame, WEBSOCKET_FIN_BIT | WebsocketPongOpcode); - Stream_Write_UINT8(closeFrame, closeDataLen | WEBSOCKET_MASK_BIT); /* no payload */ - winpr_RAND(&maskingKey1, 2); - winpr_RAND(&maskingKey2, 2); - Stream_Write_UINT16(closeFrame, maskingKey1); - Stream_Write_UINT16(closeFrame, maskingKey2); /* unused half, max 2 bytes of data */ - - if (closeDataLen == 2) - { - uint16_t data; - Stream_Read_UINT16(s, data); - Stream_Write_UINT16(closeFrame, data ^ maskingKey1); - } - Stream_SealLength(closeFrame); - - ERR_clear_error(); - status = BIO_write(bio, Stream_Buffer(closeFrame), Stream_Length(closeFrame)); - Stream_Free(closeFrame, TRUE); - - /* server MUST close socket now. The server is not allowed anymore to - * send frames but if he does, nothing bad would happen */ - if (status < 0) - return FALSE; - return TRUE; -} - -static BOOL rdg_websocket_reply_pong(BIO* bio, wStream* s) -{ - wStream* closeFrame; - uint32_t maskingKey; - int status; - - if (s != NULL) - return rdg_write_websocket(bio, s, WebsocketPongOpcode); - - closeFrame = Stream_New(NULL, 6); - if (!closeFrame) - return FALSE; - - Stream_Write_UINT8(closeFrame, WEBSOCKET_FIN_BIT | WebsocketPongOpcode); - Stream_Write_UINT8(closeFrame, 0 | WEBSOCKET_MASK_BIT); /* no payload */ - winpr_RAND(&maskingKey, 4); - Stream_Write_UINT32(closeFrame, maskingKey); /* dummy masking key. */ - Stream_SealLength(closeFrame); - - ERR_clear_error(); - status = BIO_write(bio, Stream_Buffer(closeFrame), Stream_Length(closeFrame)); - Stream_Free(closeFrame, TRUE); - - if (status < 0) - return FALSE; - return TRUE; -} - -static int rdg_websocket_handle_payload(BIO* bio, BYTE* pBuffer, size_t size, - rdg_http_websocket_context* encodingContext) -{ - int status; - BYTE effectiveOpcode = ((encodingContext->opcode & 0xf) == WebsocketContinuationOpcode - ? encodingContext->fragmentOriginalOpcode & 0xf - : encodingContext->opcode & 0xf); - - switch (effectiveOpcode) - { - case WebsocketBinaryOpcode: - { - status = rdg_websocket_read_data(bio, pBuffer, size, encodingContext); - if (status < 0) - return status; - - return status; - } - case WebsocketPingOpcode: - { - if (encodingContext->responseStreamBuffer == NULL) - encodingContext->responseStreamBuffer = - Stream_New(NULL, encodingContext->payloadLength); - - status = rdg_websocket_read_wstream(bio, encodingContext->responseStreamBuffer, - encodingContext); - if (status < 0) - return status; - - if (encodingContext->payloadLength == 0) - { - if (!encodingContext->closeSent) - rdg_websocket_reply_pong(bio, encodingContext->responseStreamBuffer); - - if (encodingContext->responseStreamBuffer) - Stream_Free(encodingContext->responseStreamBuffer, TRUE); - encodingContext->responseStreamBuffer = NULL; - } - } - break; - case WebsocketCloseOpcode: - { - if (encodingContext->responseStreamBuffer == NULL) - encodingContext->responseStreamBuffer = - Stream_New(NULL, encodingContext->payloadLength); - - status = rdg_websocket_read_wstream(bio, encodingContext->responseStreamBuffer, - encodingContext); - if (status < 0) - return status; - - if (encodingContext->payloadLength == 0) - { - rdg_websocket_reply_close(bio, encodingContext->responseStreamBuffer); - encodingContext->closeSent = TRUE; - - if (encodingContext->responseStreamBuffer) - Stream_Free(encodingContext->responseStreamBuffer, TRUE); - encodingContext->responseStreamBuffer = NULL; - } - } - break; - default: - WLog_WARN(TAG, "Unimplemented websocket opcode %x. Dropping", effectiveOpcode & 0xf); - - status = rdg_websocket_read_discard(bio, encodingContext); - if (status < 0) - return status; - } - /* return how many bytes have been written to pBuffer. - * Only WebsocketBinaryOpcode writes into it and it returns directly */ - return 0; -} - -static int rdg_websocket_read(BIO* bio, BYTE* pBuffer, size_t size, - rdg_http_websocket_context* encodingContext) -{ - int status; - int effectiveDataLen = 0; - WINPR_ASSERT(encodingContext != NULL); - while (TRUE) - { - switch (encodingContext->state) - { - case WebsocketStateOpcodeAndFin: - { - BYTE buffer[1]; - ERR_clear_error(); - status = BIO_read(bio, (char*)buffer, 1); - if (status <= 0) - return (effectiveDataLen > 0 ? effectiveDataLen : status); - - encodingContext->opcode = buffer[0]; - if (((encodingContext->opcode & 0xf) != WebsocketContinuationOpcode) && - (encodingContext->opcode & 0xf) < 0x08) - encodingContext->fragmentOriginalOpcode = encodingContext->opcode; - encodingContext->state = WebsocketStateLengthAndMasking; - } - break; - case WebsocketStateLengthAndMasking: - { - BYTE buffer[1]; - BYTE len; - ERR_clear_error(); - status = BIO_read(bio, (char*)buffer, 1); - if (status <= 0) - return (effectiveDataLen > 0 ? effectiveDataLen : status); - - encodingContext->masking = ((buffer[0] & WEBSOCKET_MASK_BIT) == WEBSOCKET_MASK_BIT); - encodingContext->lengthAndMaskPosition = 0; - encodingContext->payloadLength = 0; - len = buffer[0] & 0x7f; - if (len < 126) - { - encodingContext->payloadLength = len; - encodingContext->state = (encodingContext->masking ? WebSocketStateMaskingKey - : WebSocketStatePayload); - } - else if (len == 126) - encodingContext->state = WebsocketStateShortLength; - else - encodingContext->state = WebsocketStateLongLength; - } - break; - case WebsocketStateShortLength: - case WebsocketStateLongLength: - { - BYTE buffer[1]; - BYTE lenLength = (encodingContext->state == WebsocketStateShortLength ? 2 : 8); - while (encodingContext->lengthAndMaskPosition < lenLength) - { - ERR_clear_error(); - status = BIO_read(bio, (char*)buffer, 1); - if (status <= 0) - return (effectiveDataLen > 0 ? effectiveDataLen : status); - - encodingContext->payloadLength = - (encodingContext->payloadLength) << 8 | buffer[0]; - encodingContext->lengthAndMaskPosition += status; - } - encodingContext->state = - (encodingContext->masking ? WebSocketStateMaskingKey : WebSocketStatePayload); - } - break; - case WebSocketStateMaskingKey: - { - WLog_WARN( - TAG, "Websocket Server sends data with masking key. This is against RFC 6455."); - return -1; - } - case WebSocketStatePayload: - { - status = rdg_websocket_handle_payload(bio, pBuffer, size, encodingContext); - if (status < 0) - return (effectiveDataLen > 0 ? effectiveDataLen : status); - - effectiveDataLen += status; - - if ((size_t)status == size) - return effectiveDataLen; - pBuffer += status; - size -= status; - } - } - } - /* should be unreachable */ -} - static int rdg_chuncked_read(BIO* bio, BYTE* pBuffer, size_t size, rdg_http_encoding_chunked_context* encodingContext) { @@ -905,7 +464,7 @@ static int rdg_socket_read(BIO* bio, BYTE* pBuffer, size_t size, if (encodingContext->isWebsocketTransport) { - return rdg_websocket_read(bio, pBuffer, size, &encodingContext->context.websocket); + return websocket_read(bio, pBuffer, size, &encodingContext->context.websocket); } switch (encodingContext->httpTransferEncoding) diff --git a/libfreerdp/core/gateway/rdg.h b/libfreerdp/core/gateway/rdg.h index 19a0d10b0..5b0249cdc 100644 --- a/libfreerdp/core/gateway/rdg.h +++ b/libfreerdp/core/gateway/rdg.h @@ -22,21 +22,12 @@ #include #include -#include -#include -#include -#include -#include - -#include -#include -#include +/* needed for BIO */ +#include typedef struct rdp_rdg rdpRdg; -#include "http.h" - FREERDP_LOCAL rdpRdg* rdg_new(rdpContext* context); FREERDP_LOCAL void rdg_free(rdpRdg* rdg); diff --git a/libfreerdp/core/gateway/websocket.c b/libfreerdp/core/gateway/websocket.c new file mode 100644 index 000000000..d7086284d --- /dev/null +++ b/libfreerdp/core/gateway/websocket.c @@ -0,0 +1,521 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Websocket Framing + * + * Copyright 2023 Michael Saxl + * + * 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 "websocket.h" +#include + +#define TAG FREERDP_TAG("core.gateway.websocket") + +BOOL websocket_write_wstream(BIO* bio, wStream* sPacket, WEBSOCKET_OPCODE opcode) +{ + size_t fullLen; + int status; + wStream* sWS; + + uint32_t maskingKey; + + size_t streamPos; + + WINPR_ASSERT(bio); + WINPR_ASSERT(sPacket); + + const size_t len = Stream_Length(sPacket); + Stream_SetPosition(sPacket, 0); + + if (len > INT_MAX) + return FALSE; + + if (len < 126) + fullLen = len + 6; /* 2 byte "mini header" + 4 byte masking key */ + else if (len < 0x10000) + fullLen = len + 8; /* 2 byte "mini header" + 2 byte length + 4 byte masking key */ + else + fullLen = len + 14; /* 2 byte "mini header" + 8 byte length + 4 byte masking key */ + + sWS = Stream_New(NULL, fullLen); + if (!sWS) + return FALSE; + + winpr_RAND(&maskingKey, sizeof(maskingKey)); + + Stream_Write_UINT8(sWS, WEBSOCKET_FIN_BIT | opcode); + if (len < 126) + Stream_Write_UINT8(sWS, len | WEBSOCKET_MASK_BIT); + else if (len < 0x10000) + { + Stream_Write_UINT8(sWS, 126 | WEBSOCKET_MASK_BIT); + Stream_Write_UINT16_BE(sWS, len); + } + else + { + Stream_Write_UINT8(sWS, 127 | WEBSOCKET_MASK_BIT); + Stream_Write_UINT32_BE(sWS, 0); /* payload is limited to INT_MAX */ + Stream_Write_UINT32_BE(sWS, len); + } + Stream_Write_UINT32(sWS, maskingKey); + + /* mask as much as possible with 32bit access */ + for (streamPos = 0; streamPos + 4 <= len; streamPos += 4) + { + uint32_t data; + Stream_Read_UINT32(sPacket, data); + Stream_Write_UINT32(sWS, data ^ maskingKey); + } + + /* mask the rest byte by byte */ + for (; streamPos < len; streamPos++) + { + BYTE data; + BYTE* partialMask = ((BYTE*)&maskingKey) + (streamPos % 4); + Stream_Read_UINT8(sPacket, data); + Stream_Write_UINT8(sWS, data ^ *partialMask); + } + + Stream_SealLength(sWS); + + ERR_clear_error(); + status = BIO_write(bio, Stream_Buffer(sWS), Stream_Length(sWS)); + Stream_Free(sWS, TRUE); + + if (status != (SSIZE_T)fullLen) + return FALSE; + + return TRUE; +} + +int websocket_write(BIO* bio, const BYTE* buf, int isize, WEBSOCKET_OPCODE opcode) +{ + size_t payloadSize; + size_t fullLen; + int status; + wStream* sWS; + + uint32_t maskingKey; + + int streamPos; + + WINPR_ASSERT(bio); + WINPR_ASSERT(buf); + + winpr_RAND(&maskingKey, sizeof(maskingKey)); + + payloadSize = isize; + if ((isize < 0) || (isize > UINT16_MAX)) + return -1; + + if (payloadSize < 126) + fullLen = payloadSize + 6; /* 2 byte "mini header" + 4 byte masking key */ + else if (payloadSize < 0x10000) + fullLen = payloadSize + 8; /* 2 byte "mini header" + 2 byte length + 4 byte masking key */ + else + fullLen = payloadSize + 14; /* 2 byte "mini header" + 8 byte length + 4 byte masking key */ + + sWS = Stream_New(NULL, fullLen); + if (!sWS) + return FALSE; + + Stream_Write_UINT8(sWS, WEBSOCKET_FIN_BIT | opcode); + if (payloadSize < 126) + Stream_Write_UINT8(sWS, payloadSize | WEBSOCKET_MASK_BIT); + else if (payloadSize < 0x10000) + { + Stream_Write_UINT8(sWS, 126 | WEBSOCKET_MASK_BIT); + Stream_Write_UINT16_BE(sWS, payloadSize); + } + else + { + Stream_Write_UINT8(sWS, 127 | WEBSOCKET_MASK_BIT); + /* biggest packet possible is 0xffff + 0xa, so 32bit is always enough */ + Stream_Write_UINT32_BE(sWS, 0); + Stream_Write_UINT32_BE(sWS, payloadSize); + } + Stream_Write_UINT32(sWS, maskingKey); + + /* mask as much as possible with 32bit access */ + for (streamPos = 0; streamPos + 4 <= isize; streamPos += 4) + { + uint32_t masked = *((const uint32_t*)((const BYTE*)buf + streamPos)) ^ maskingKey; + Stream_Write_UINT32(sWS, masked); + } + + /* mask the rest byte by byte */ + for (; streamPos < isize; streamPos++) + { + BYTE* partialMask = (BYTE*)(&maskingKey) + streamPos % 4; + BYTE masked = *((const BYTE*)((const BYTE*)buf + streamPos)) ^ *partialMask; + Stream_Write_UINT8(sWS, masked); + } + + Stream_SealLength(sWS); + + status = BIO_write(bio, Stream_Buffer(sWS), Stream_Length(sWS)); + Stream_Free(sWS, TRUE); + + if (status < 0) + return status; + + return isize; +} + +static int websocket_read_data(BIO* bio, BYTE* pBuffer, size_t size, + websocket_context* encodingContext) +{ + int status; + + WINPR_ASSERT(bio); + WINPR_ASSERT(pBuffer); + WINPR_ASSERT(encodingContext); + + if (encodingContext->payloadLength == 0) + { + encodingContext->state = WebsocketStateOpcodeAndFin; + return 0; + } + + ERR_clear_error(); + status = + BIO_read(bio, pBuffer, + (encodingContext->payloadLength < size ? encodingContext->payloadLength : size)); + if (status <= 0) + return status; + + encodingContext->payloadLength -= status; + + if (encodingContext->payloadLength == 0) + encodingContext->state = WebsocketStateOpcodeAndFin; + + return status; +} + +static int websocket_read_discard(BIO* bio, websocket_context* encodingContext) +{ + char _dummy[256] = { 0 }; + int status; + + WINPR_ASSERT(bio); + WINPR_ASSERT(encodingContext); + + if (encodingContext->payloadLength == 0) + { + encodingContext->state = WebsocketStateOpcodeAndFin; + return 0; + } + + ERR_clear_error(); + status = BIO_read(bio, _dummy, sizeof(_dummy)); + if (status <= 0) + return status; + + encodingContext->payloadLength -= status; + + if (encodingContext->payloadLength == 0) + encodingContext->state = WebsocketStateOpcodeAndFin; + + return status; +} + +static int websocket_read_wstream(BIO* bio, wStream* s, websocket_context* encodingContext) +{ + int status; + + WINPR_ASSERT(bio); + WINPR_ASSERT(s); + WINPR_ASSERT(encodingContext); + + if (encodingContext->payloadLength == 0) + { + encodingContext->state = WebsocketStateOpcodeAndFin; + return 0; + } + if (Stream_GetRemainingCapacity(s) != encodingContext->payloadLength) + { + WLog_WARN(TAG, + "wStream::capacity [%" PRIuz "] != encodingContext::paylaodLangth [%" PRIuz "]", + Stream_GetRemainingCapacity(s), encodingContext->payloadLength); + return -1; + } + + ERR_clear_error(); + status = BIO_read(bio, Stream_Pointer(s), encodingContext->payloadLength); + if (status <= 0) + return status; + + Stream_Seek(s, status); + + encodingContext->payloadLength -= status; + + if (encodingContext->payloadLength == 0) + { + encodingContext->state = WebsocketStateOpcodeAndFin; + Stream_SealLength(s); + Stream_SetPosition(s, 0); + } + + return status; +} + +static BOOL websocket_reply_close(BIO* bio, wStream* s) +{ + /* write back close */ + wStream* closeFrame; + uint16_t maskingKey1; + uint16_t maskingKey2; + int status; + size_t closeDataLen; + + WINPR_ASSERT(bio); + + closeDataLen = 0; + if (s != NULL && Stream_Length(s) >= 2) + closeDataLen = 2; + + closeFrame = Stream_New(NULL, 6 + closeDataLen); + if (!closeFrame) + return FALSE; + + Stream_Write_UINT8(closeFrame, WEBSOCKET_FIN_BIT | WebsocketCloseOpcode); + Stream_Write_UINT8(closeFrame, closeDataLen | WEBSOCKET_MASK_BIT); /* no payload */ + winpr_RAND(&maskingKey1, sizeof(maskingKey1)); + winpr_RAND(&maskingKey2, sizeof(maskingKey2)); + Stream_Write_UINT16(closeFrame, maskingKey1); + Stream_Write_UINT16(closeFrame, maskingKey2); /* unused half, max 2 bytes of data */ + + if (closeDataLen == 2) + { + uint16_t data; + Stream_Read_UINT16(s, data); + Stream_Write_UINT16(closeFrame, data ^ maskingKey1); + } + Stream_SealLength(closeFrame); + + ERR_clear_error(); + status = BIO_write(bio, Stream_Buffer(closeFrame), Stream_Length(closeFrame)); + Stream_Free(closeFrame, TRUE); + + /* server MUST close socket now. The server is not allowed anymore to + * send frames but if he does, nothing bad would happen */ + if (status < 0) + return FALSE; + return TRUE; +} + +static BOOL websocket_reply_pong(BIO* bio, wStream* s) +{ + wStream* closeFrame; + uint32_t maskingKey; + int status; + + WINPR_ASSERT(bio); + + if (s != NULL) + return websocket_write_wstream(bio, s, WebsocketPongOpcode); + + closeFrame = Stream_New(NULL, 6); + if (!closeFrame) + return FALSE; + + Stream_Write_UINT8(closeFrame, WEBSOCKET_FIN_BIT | WebsocketPongOpcode); + Stream_Write_UINT8(closeFrame, 0 | WEBSOCKET_MASK_BIT); /* no payload */ + winpr_RAND(&maskingKey, sizeof(maskingKey)); + Stream_Write_UINT32(closeFrame, maskingKey); /* dummy masking key. */ + Stream_SealLength(closeFrame); + + ERR_clear_error(); + status = BIO_write(bio, Stream_Buffer(closeFrame), Stream_Length(closeFrame)); + Stream_Free(closeFrame, TRUE); + + if (status < 0) + return FALSE; + return TRUE; +} + +static int websocket_handle_payload(BIO* bio, BYTE* pBuffer, size_t size, + websocket_context* encodingContext) +{ + int status; + + WINPR_ASSERT(bio); + WINPR_ASSERT(pBuffer); + WINPR_ASSERT(encodingContext); + + BYTE effectiveOpcode = ((encodingContext->opcode & 0xf) == WebsocketContinuationOpcode + ? encodingContext->fragmentOriginalOpcode & 0xf + : encodingContext->opcode & 0xf); + + switch (effectiveOpcode) + { + case WebsocketBinaryOpcode: + { + status = websocket_read_data(bio, pBuffer, size, encodingContext); + if (status < 0) + return status; + + return status; + } + case WebsocketPingOpcode: + { + if (encodingContext->responseStreamBuffer == NULL) + encodingContext->responseStreamBuffer = + Stream_New(NULL, encodingContext->payloadLength); + + status = + websocket_read_wstream(bio, encodingContext->responseStreamBuffer, encodingContext); + if (status < 0) + return status; + + if (encodingContext->payloadLength == 0) + { + if (!encodingContext->closeSent) + websocket_reply_pong(bio, encodingContext->responseStreamBuffer); + + Stream_Free(encodingContext->responseStreamBuffer, TRUE); + encodingContext->responseStreamBuffer = NULL; + } + } + break; + case WebsocketCloseOpcode: + { + if (encodingContext->responseStreamBuffer == NULL) + encodingContext->responseStreamBuffer = + Stream_New(NULL, encodingContext->payloadLength); + + status = + websocket_read_wstream(bio, encodingContext->responseStreamBuffer, encodingContext); + if (status < 0) + return status; + + if (encodingContext->payloadLength == 0) + { + websocket_reply_close(bio, encodingContext->responseStreamBuffer); + encodingContext->closeSent = TRUE; + + if (encodingContext->responseStreamBuffer) + Stream_Free(encodingContext->responseStreamBuffer, TRUE); + encodingContext->responseStreamBuffer = NULL; + } + } + break; + default: + WLog_WARN(TAG, "Unimplemented websocket opcode %x. Dropping", effectiveOpcode & 0xf); + + status = websocket_read_discard(bio, encodingContext); + if (status < 0) + return status; + } + /* return how many bytes have been written to pBuffer. + * Only WebsocketBinaryOpcode writes into it and it returns directly */ + return 0; +} + +int websocket_read(BIO* bio, BYTE* pBuffer, size_t size, websocket_context* encodingContext) +{ + int status; + int effectiveDataLen = 0; + + WINPR_ASSERT(bio); + WINPR_ASSERT(pBuffer); + WINPR_ASSERT(encodingContext); + + while (TRUE) + { + switch (encodingContext->state) + { + case WebsocketStateOpcodeAndFin: + { + BYTE buffer[1]; + ERR_clear_error(); + status = BIO_read(bio, (char*)buffer, sizeof(buffer)); + if (status <= 0) + return (effectiveDataLen > 0 ? effectiveDataLen : status); + + encodingContext->opcode = buffer[0]; + if (((encodingContext->opcode & 0xf) != WebsocketContinuationOpcode) && + (encodingContext->opcode & 0xf) < 0x08) + encodingContext->fragmentOriginalOpcode = encodingContext->opcode; + encodingContext->state = WebsocketStateLengthAndMasking; + } + break; + case WebsocketStateLengthAndMasking: + { + BYTE buffer[1]; + BYTE len; + ERR_clear_error(); + status = BIO_read(bio, (char*)buffer, sizeof(buffer)); + if (status <= 0) + return (effectiveDataLen > 0 ? effectiveDataLen : status); + + encodingContext->masking = ((buffer[0] & WEBSOCKET_MASK_BIT) == WEBSOCKET_MASK_BIT); + encodingContext->lengthAndMaskPosition = 0; + encodingContext->payloadLength = 0; + len = buffer[0] & 0x7f; + if (len < 126) + { + encodingContext->payloadLength = len; + encodingContext->state = (encodingContext->masking ? WebSocketStateMaskingKey + : WebSocketStatePayload); + } + else if (len == 126) + encodingContext->state = WebsocketStateShortLength; + else + encodingContext->state = WebsocketStateLongLength; + } + break; + case WebsocketStateShortLength: + case WebsocketStateLongLength: + { + BYTE buffer[1]; + BYTE lenLength = (encodingContext->state == WebsocketStateShortLength ? 2 : 8); + while (encodingContext->lengthAndMaskPosition < lenLength) + { + ERR_clear_error(); + status = BIO_read(bio, (char*)buffer, sizeof(buffer)); + if (status <= 0) + return (effectiveDataLen > 0 ? effectiveDataLen : status); + + encodingContext->payloadLength = + (encodingContext->payloadLength) << 8 | buffer[0]; + encodingContext->lengthAndMaskPosition += status; + } + encodingContext->state = + (encodingContext->masking ? WebSocketStateMaskingKey : WebSocketStatePayload); + } + break; + case WebSocketStateMaskingKey: + { + WLog_WARN( + TAG, "Websocket Server sends data with masking key. This is against RFC 6455."); + return -1; + } + case WebSocketStatePayload: + { + status = websocket_handle_payload(bio, pBuffer, size, encodingContext); + if (status < 0) + return (effectiveDataLen > 0 ? effectiveDataLen : status); + + effectiveDataLen += status; + + if ((size_t)status == size) + return effectiveDataLen; + pBuffer += status; + size -= status; + } + } + } + /* should be unreachable */ +} diff --git a/libfreerdp/core/gateway/websocket.h b/libfreerdp/core/gateway/websocket.h new file mode 100644 index 000000000..de41e4908 --- /dev/null +++ b/libfreerdp/core/gateway/websocket.h @@ -0,0 +1,71 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Websocket Framing + * + * Copyright 2023 Michael Saxl + * + * 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. + */ + +#ifndef FREERDP_LIB_CORE_GATEWAY_WEBSOCKET_H +#define FREERDP_LIB_CORE_GATEWAY_WEBSOCKET_H + +#include +#include + +#include + +#include "../../crypto/tls.h" + +#define WEBSOCKET_MASK_BIT 0x80 +#define WEBSOCKET_FIN_BIT 0x80 + +typedef enum +{ + WebsocketContinuationOpcode = 0x0, + WebsocketTextOpcode = 0x1, + WebsocketBinaryOpcode = 0x2, + WebsocketCloseOpcode = 0x8, + WebsocketPingOpcode = 0x9, + WebsocketPongOpcode = 0xa, +} WEBSOCKET_OPCODE; + +typedef enum +{ + WebsocketStateOpcodeAndFin, + WebsocketStateLengthAndMasking, + WebsocketStateShortLength, + WebsocketStateLongLength, + WebSocketStateMaskingKey, + WebSocketStatePayload, +} WEBSOCKET_STATE; + +typedef struct +{ + size_t payloadLength; + uint32_t maskingKey; + BOOL masking; + BOOL closeSent; + BYTE opcode; + BYTE fragmentOriginalOpcode; + BYTE lengthAndMaskPosition; + WEBSOCKET_STATE state; + wStream* responseStreamBuffer; +} websocket_context; + +FREERDP_LOCAL BOOL websocket_write_wstream(BIO* bio, wStream* sPacket, WEBSOCKET_OPCODE opcode); +FREERDP_LOCAL int websocket_write(BIO* bio, const BYTE* buf, int isize, WEBSOCKET_OPCODE opcode); +FREERDP_LOCAL int websocket_read(BIO* bio, BYTE* pBuffer, size_t size, + websocket_context* encodingContext); + +#endif /* FREERDP_LIB_CORE_GATEWAY_WEBSOCKET_H */ diff --git a/libfreerdp/core/gateway/wst.c b/libfreerdp/core/gateway/wst.c new file mode 100644 index 000000000..382335966 --- /dev/null +++ b/libfreerdp/core/gateway/wst.c @@ -0,0 +1,773 @@ +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "wst.h" +#include "websocket.h" +#include "http.h" +#include "../credssp_auth.h" +#include "../proxy.h" +#include "../rdp.h" +#include "../../crypto/opensslcompat.h" +#include "rpc_fault.h" +#include "../utils.h" +#include "../settings.h" + +#define TAG FREERDP_TAG("core.gateway.wst") + +#define AUTH_PKG NEGO_SSP_NAME + +struct rdp_wst +{ + rdpContext* context; + rdpSettings* settings; + BOOL attached; + BIO* frontBio; + rdpTls* tls; + rdpCredsspAuth* auth; + HttpContext* http; + CRITICAL_SECTION writeSection; + char* gwhostname; + uint16_t gwport; + char* gwpath; + websocket_context wscontext; +}; + +static BOOL wst_get_gateway_credentials(rdpContext* context, rdp_auth_reason reason) +{ + WINPR_ASSERT(context); + freerdp* instance = context->instance; + + auth_status rc = utils_authenticate_gateway(instance, reason); + switch (rc) + { + case AUTH_SUCCESS: + case AUTH_SKIP: + return TRUE; + case AUTH_CANCELLED: + freerdp_set_last_error_log(instance->context, FREERDP_ERROR_CONNECT_CANCELLED); + return FALSE; + case AUTH_NO_CREDENTIALS: + freerdp_set_last_error_log(instance->context, + FREERDP_ERROR_CONNECT_NO_OR_MISSING_CREDENTIALS); + return FALSE; + case AUTH_FAILED: + default: + return FALSE; + } +} + +static BOOL wst_auth_init(rdpWst* wst, rdpTls* tls, TCHAR* authPkg) +{ + WINPR_ASSERT(wst); + WINPR_ASSERT(tls); + WINPR_ASSERT(authPkg); + + rdpContext* context = wst->context; + rdpSettings* settings = context->settings; + SEC_WINNT_AUTH_IDENTITY identity = { 0 }; + int rc; + + wst->auth = credssp_auth_new(context); + if (!wst->auth) + return FALSE; + + if (!credssp_auth_init(wst->auth, authPkg, tls->Bindings)) + return FALSE; + + if (!wst_get_gateway_credentials(context, GW_AUTH_RDG)) + return FALSE; + + if (!identity_set_from_settings(&identity, settings, FreeRDP_GatewayUsername, + FreeRDP_GatewayDomain, FreeRDP_GatewayPassword)) + return FALSE; + + if (!credssp_auth_setup_client(wst->auth, "HTTP", wst->gwhostname, &identity, NULL)) + { + sspi_FreeAuthIdentity(&identity); + return FALSE; + } + sspi_FreeAuthIdentity(&identity); + + credssp_auth_set_flags(wst->auth, ISC_REQ_CONFIDENTIALITY | ISC_REQ_MUTUAL_AUTH); + + rc = credssp_auth_authenticate(wst->auth); + if (rc < 0) + return FALSE; + + return TRUE; +} + +static BOOL wst_set_auth_header(rdpCredsspAuth* auth, HttpRequest* request) +{ + WINPR_ASSERT(auth); + WINPR_ASSERT(request); + + const SecBuffer* authToken = credssp_auth_get_output_buffer(auth); + char* base64AuthToken = NULL; + + if (authToken) + { + if (authToken->cbBuffer > INT_MAX) + return FALSE; + + base64AuthToken = crypto_base64_encode(authToken->pvBuffer, (int)authToken->cbBuffer); + } + + if (base64AuthToken) + { + BOOL rc = http_request_set_auth_scheme(request, credssp_auth_pkg_name(auth)) && + http_request_set_auth_param(request, base64AuthToken); + free(base64AuthToken); + + if (!rc) + return FALSE; + } + + return TRUE; +} + +static BOOL wst_recv_auth_token(rdpCredsspAuth* auth, HttpResponse* response) +{ + char buffer[64] = { 0 }; + size_t len; + const char* token64 = NULL; + size_t authTokenLength = 0; + BYTE* authTokenData = NULL; + SecBuffer authToken = { 0 }; + long StatusCode; + int rc; + + if (!auth || !response) + return FALSE; + + StatusCode = http_response_get_status_code(response); + switch (StatusCode) + { + case HTTP_STATUS_DENIED: + case HTTP_STATUS_OK: + break; + default: + WLog_WARN(TAG, "Unexpected HTTP status: %s", + http_status_string_format(StatusCode, buffer, ARRAYSIZE(buffer))); + return FALSE; + } + + token64 = http_response_get_auth_token(response, credssp_auth_pkg_name(auth)); + + if (!token64) + return FALSE; + + len = strlen(token64); + + crypto_base64_decode(token64, len, &authTokenData, &authTokenLength); + + if (authTokenLength && authTokenData) + { + authToken.pvBuffer = authTokenData; + authToken.cbBuffer = authTokenLength; + credssp_auth_take_input_buffer(auth, &authToken); + } + + rc = credssp_auth_authenticate(auth); + if (rc < 0) + return FALSE; + + return TRUE; +} + +static BOOL wst_tls_connect(rdpWst* wst, rdpTls* tls, int timeout) +{ + WINPR_ASSERT(wst); + WINPR_ASSERT(tls); + int sockfd = 0; + long status = 0; + BIO* socketBio = NULL; + BIO* bufferedBio = NULL; + rdpSettings* settings = wst->settings; + const char* peerHostname = wst->gwhostname; + UINT16 peerPort = wst->gwport; + const char *proxyUsername, *proxyPassword; + BOOL isProxyConnection = + proxy_prepare(settings, &peerHostname, &peerPort, &proxyUsername, &proxyPassword); + + sockfd = freerdp_tcp_connect(wst->context, peerHostname, peerPort, timeout); + + WLog_ERR(TAG, "connecting to %s %d", peerHostname, peerPort); + if (sockfd < 0) + { + return FALSE; + } + + socketBio = BIO_new(BIO_s_simple_socket()); + + if (!socketBio) + { + closesocket((SOCKET)sockfd); + return FALSE; + } + + BIO_set_fd(socketBio, sockfd, BIO_CLOSE); + bufferedBio = BIO_new(BIO_s_buffered_socket()); + + if (!bufferedBio) + { + BIO_free_all(socketBio); + return FALSE; + } + + bufferedBio = BIO_push(bufferedBio, socketBio); + status = BIO_set_nonblock(bufferedBio, TRUE); + + if (isProxyConnection) + { + if (!proxy_connect(settings, bufferedBio, proxyUsername, proxyPassword, + settings->GatewayHostname, (UINT16)settings->GatewayPort)) + { + BIO_free_all(bufferedBio); + return FALSE; + } + } + + if (!status) + { + BIO_free_all(bufferedBio); + return FALSE; + } + + tls->hostname = wst->gwhostname; // settings->GatewayHostname; + tls->port = wst->gwport; //(int)settings->GatewayPort; + tls->isGatewayTransport = TRUE; + status = freerdp_tls_connect(tls, bufferedBio); + if (status < 1) + { + rdpContext* context = wst->context; + if (status < 0) + { + freerdp_set_last_error_if_not(context, FREERDP_ERROR_TLS_CONNECT_FAILED); + } + else + { + freerdp_set_last_error_if_not(context, FREERDP_ERROR_CONNECT_CANCELLED); + } + + return FALSE; + } + return (status >= 1); +} + +static wStream* wst_build_http_request(rdpWst* wst) +{ + wStream* s = NULL; + HttpRequest* request = NULL; + const char* uri; + + if (!wst) + return NULL; + + uri = http_context_get_uri(wst->http); + request = http_request_new(); + + if (!request) + return NULL; + + if (!http_request_set_method(request, "GET") || !http_request_set_uri(request, uri)) + goto out; + + if (wst->auth) + { + if (!wst_set_auth_header(wst->auth, request)) + goto out; + } + else if (wst->settings->GatewayHttpExtAuthBearer) + { + http_request_set_auth_scheme(request, "Bearer"); + http_request_set_auth_param(request, wst->settings->GatewayHttpExtAuthBearer); + } + + s = http_request_write(wst->http, request); +out: + http_request_free(request); + + if (s) + Stream_SealLength(s); + + return s; +} + +static BOOL wst_send_http_request(rdpWst* wst, rdpTls* tls) +{ + size_t sz; + wStream* s = NULL; + int status = -1; + WINPR_ASSERT(wst); + WINPR_ASSERT(tls); + + s = wst_build_http_request(wst); + + if (!s) + return FALSE; + + sz = Stream_Length(s); + + if (sz <= INT_MAX) + status = freerdp_tls_write_all(tls, Stream_Buffer(s), (int)sz); + + Stream_Free(s, TRUE); + return (status >= 0); +} + +BOOL wst_connect(rdpWst* wst, DWORD timeout) +{ + HttpResponse* response = NULL; + long StatusCode; + + WINPR_ASSERT(wst); + if (!wst_tls_connect(wst, wst->tls, timeout)) + return FALSE; + + if (!wst_send_http_request(wst, wst->tls)) + return FALSE; + + response = http_response_recv(wst->tls, TRUE); + if (!response) + { + return FALSE; + } + + StatusCode = http_response_get_status_code(response); + + switch (StatusCode) + { + case HTTP_STATUS_NOT_FOUND: + case HTTP_STATUS_BAD_METHOD: + { + http_response_free(response); + return FALSE; + } + case HTTP_STATUS_DENIED: + { + /* if the response is HTTP_STATUS_DENIED and no bearer authentication was used, try with + * credssp */ + http_response_free(response); + if (wst->settings->GatewayHttpExtAuthBearer) + return FALSE; + + if (!wst_auth_init(wst, wst->tls, AUTH_PKG)) + return FALSE; + if (!wst_send_http_request(wst, wst->tls)) + return FALSE; + + response = http_response_recv(wst->tls, TRUE); + if (!response) + { + return FALSE; + } + while (!credssp_auth_is_complete(wst->auth)) + { + if (!wst_recv_auth_token(wst->auth, response)) + { + http_response_free(response); + return FALSE; + } + + if (credssp_auth_have_output_token(wst->auth)) + { + http_response_free(response); + + if (!wst_send_http_request(wst, wst->tls)) + return FALSE; + + response = http_response_recv(wst->tls, TRUE); + if (!response) + { + return FALSE; + } + } + } + StatusCode = http_response_get_status_code(response); + credssp_auth_free(wst->auth); + wst->auth = NULL; + break; + } + default: + break; + } + http_response_free(response); + + if (StatusCode == HTTP_STATUS_SWITCH_PROTOCOLS) + { + wst->wscontext.state = WebsocketStateOpcodeAndFin; + wst->wscontext.responseStreamBuffer = NULL; + return TRUE; + } + else + { + char buffer[64] = { 0 }; + WLog_ERR(TAG, "Unexpected HTTP status: %s", + http_status_string_format(StatusCode, buffer, ARRAYSIZE(buffer))); + } + return FALSE; +} + +DWORD wst_get_event_handles(rdpWst* wst, HANDLE* events, DWORD count) +{ + DWORD nCount = 0; + WINPR_ASSERT(wst != NULL); + + if (wst->tls) + { + if (events && (nCount < count)) + { + BIO_get_event(wst->tls->bio, &events[nCount]); + nCount++; + } + else + return 0; + } + + return nCount; +} + +static int wst_bio_write(BIO* bio, const char* buf, int num) +{ + int status; + WINPR_ASSERT(bio); + WINPR_ASSERT(buf); + + rdpWst* wst = (rdpWst*)BIO_get_data(bio); + WINPR_ASSERT(wst); + BIO_clear_flags(bio, BIO_FLAGS_WRITE); + EnterCriticalSection(&wst->writeSection); + status = websocket_write(wst->tls->bio, (const BYTE*)buf, num, WebsocketBinaryOpcode); + LeaveCriticalSection(&wst->writeSection); + + if (status < 0) + { + BIO_clear_flags(bio, BIO_FLAGS_SHOULD_RETRY); + return -1; + } + else if (status < num) + { + BIO_set_flags(bio, BIO_FLAGS_WRITE); + WSASetLastError(WSAEWOULDBLOCK); + } + else + { + BIO_set_flags(bio, BIO_FLAGS_WRITE); + } + + return status; +} + +static int wst_bio_read(BIO* bio, char* buf, int size) +{ + int status = 0; + WINPR_ASSERT(bio); + WINPR_ASSERT(buf); + + rdpWst* wst = (rdpWst*)BIO_get_data(bio); + WINPR_ASSERT(wst); + + while (status <= 0) + { + status = websocket_read(wst->tls->bio, (BYTE*)buf, size, &wst->wscontext); + if (status <= 0) + { + if (!BIO_should_retry(wst->tls->bio)) + return -1; + return 0; + } + } + + if (status < 0) + { + BIO_clear_retry_flags(bio); + return -1; + } + else if (status == 0) + { + BIO_set_retry_read(bio); + WSASetLastError(WSAEWOULDBLOCK); + return -1; + } + else + { + BIO_set_flags(bio, BIO_FLAGS_READ); + } + + return status; +} + +static int wst_bio_puts(BIO* bio, const char* str) +{ + WINPR_UNUSED(bio); + WINPR_UNUSED(str); + return -2; +} + +static int wst_bio_gets(BIO* bio, char* str, int size) +{ + WINPR_UNUSED(bio); + WINPR_UNUSED(str); + WINPR_UNUSED(size); + return -2; +} + +static long wst_bio_ctrl(BIO* bio, int cmd, long arg1, void* arg2) +{ + long status = -1; + WINPR_ASSERT(bio); + + rdpWst* wst = (rdpWst*)BIO_get_data(bio); + WINPR_ASSERT(wst); + rdpTls* tls = wst->tls; + + if (cmd == BIO_CTRL_FLUSH) + { + (void)BIO_flush(tls->bio); + status = 1; + } + else if (cmd == BIO_C_SET_NONBLOCK) + { + status = 1; + } + else if (cmd == BIO_C_READ_BLOCKED) + { + status = BIO_read_blocked(tls->bio); + } + else if (cmd == BIO_C_WRITE_BLOCKED) + { + status = BIO_write_blocked(tls->bio); + } + else if (cmd == BIO_C_WAIT_READ) + { + int timeout = (int)arg1; + + if (BIO_read_blocked(tls->bio)) + return BIO_wait_read(tls->bio, timeout); + status = 1; + } + else if (cmd == BIO_C_WAIT_WRITE) + { + int timeout = (int)arg1; + + if (BIO_write_blocked(tls->bio)) + status = BIO_wait_write(tls->bio, timeout); + else + status = 1; + } + else if (cmd == BIO_C_GET_EVENT || cmd == BIO_C_GET_FD) + { + status = BIO_ctrl(tls->bio, cmd, arg1, arg2); + } +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + else if (cmd == BIO_CTRL_GET_KTLS_SEND) + { + /* Even though BIO_get_ktls_send says that returning negative values is valid + * openssl internal sources are full of if(!BIO_get_ktls_send && ) stuff. This has some + * nasty sideeffects. return 0 as proper no KTLS offloading flag + */ + status = 0; + } + else if (cmd == BIO_CTRL_GET_KTLS_RECV) + { + /* Even though BIO_get_ktls_recv says that returning negative values is valid + * there is no reason to trust trust negative values are implemented right everywhere + */ + status = 0; + } +#endif + return status; +} + +static int wst_bio_new(BIO* bio) +{ + BIO_set_init(bio, 1); + BIO_set_flags(bio, BIO_FLAGS_SHOULD_RETRY); + return 1; +} + +static int wst_bio_free(BIO* bio) +{ + WINPR_UNUSED(bio); + return 1; +} + +static BIO_METHOD* BIO_s_wst(void) +{ + static BIO_METHOD* bio_methods = NULL; + + if (bio_methods == NULL) + { + if (!(bio_methods = BIO_meth_new(BIO_TYPE_TSG, "WSTransport"))) + return NULL; + + BIO_meth_set_write(bio_methods, wst_bio_write); + BIO_meth_set_read(bio_methods, wst_bio_read); + BIO_meth_set_puts(bio_methods, wst_bio_puts); + BIO_meth_set_gets(bio_methods, wst_bio_gets); + BIO_meth_set_ctrl(bio_methods, wst_bio_ctrl); + BIO_meth_set_create(bio_methods, wst_bio_new); + BIO_meth_set_destroy(bio_methods, wst_bio_free); + } + + return bio_methods; +} + +static BOOL wst_parse_url(rdpWst* wst, const char* url) +{ + const char* hostStart; + const char* pos; + WINPR_ASSERT(wst); + WINPR_ASSERT(url); + + free(wst->gwhostname); + wst->gwhostname = NULL; + free(wst->gwpath); + wst->gwpath = NULL; + + if (strncmp("wss://", url, 6) != 0) + { + WLog_ERR(TAG, "Websocket URL is invalid. Only wss:// URLs are supported"); + return FALSE; + } + + hostStart = url + 6; + pos = hostStart; + while (*pos != '\0' && *pos != ':' && *pos != '/') + pos++; + free(wst->gwhostname); + wst->gwhostname = NULL; + if (pos - hostStart == 0) + return FALSE; + wst->gwhostname = malloc(sizeof(char) * (pos - hostStart + 1)); + if (!wst->gwhostname) + return FALSE; + strncpy(wst->gwhostname, hostStart, (pos - hostStart)); + wst->gwhostname[pos - hostStart] = '\0'; + + if (*pos == ':') + { + char port[6]; + char* portNumberEnd; + pos++; + const char* portStart = pos; + while (*pos != '\0' && *pos != '/') + pos++; + if (pos - portStart > 5 || pos - portStart == 0) + return FALSE; + strncpy(port, portStart, (pos - portStart)); + port[pos - portStart] = '\0'; + int _p = strtol(port, &portNumberEnd, 10); + if (portNumberEnd && *portNumberEnd == '\0' && _p > 0 && _p <= UINT16_MAX) + wst->gwport = _p; + else + return FALSE; + } + else + wst->gwport = 443; + wst->gwpath = strdup(pos); + if (!wst->gwpath) + return FALSE; + return TRUE; +} + +rdpWst* wst_new(rdpContext* context) +{ + rdpWst* wst; + + if (!context) + return NULL; + + wst = (rdpWst*)calloc(1, sizeof(rdpWst)); + + if (wst) + { + wst->context = context; + wst->settings = wst->context->settings; + + wst->gwhostname = NULL; + wst->gwport = 443; + wst->gwpath = NULL; + + if (!wst_parse_url(wst, context->settings->GatewayUrl)) + goto wst_alloc_error; + + wst->tls = freerdp_tls_new(wst->settings); + + wst->http = http_context_new(); + + if (!wst->http) + goto wst_alloc_error; + + if (!http_context_set_uri(wst->http, wst->gwpath) || + !http_context_set_accept(wst->http, "*/*") || + !http_context_set_cache_control(wst->http, "no-cache") || + !http_context_set_pragma(wst->http, "no-cache") || + !http_context_set_connection(wst->http, "Keep-Alive") || + !http_context_set_user_agent(wst->http, "FreeRDP/3.0") || + !http_context_set_host(wst->http, wst->gwhostname) || + !http_context_enable_websocket_upgrade(wst->http, TRUE)) + { + goto wst_alloc_error; + } + + wst->frontBio = BIO_new(BIO_s_wst()); + + if (!wst->frontBio) + goto wst_alloc_error; + + BIO_set_data(wst->frontBio, wst); + InitializeCriticalSection(&wst->writeSection); + wst->auth = NULL; + } + + return wst; +wst_alloc_error: + wst_free(wst); + return NULL; +} + +void wst_free(rdpWst* wst) +{ + if (!wst) + return; + + freerdp_tls_free(wst->tls); + http_context_free(wst->http); + credssp_auth_free(wst->auth); + free(wst->gwhostname); + free(wst->gwpath); + + if (!wst->attached) + BIO_free_all(wst->frontBio); + + DeleteCriticalSection(&wst->writeSection); + + if (wst->wscontext.responseStreamBuffer != NULL) + Stream_Free(wst->wscontext.responseStreamBuffer, TRUE); + + free(wst); +} + +BIO* wst_get_front_bio_and_take_ownership(rdpWst* wst) +{ + if (!wst) + return NULL; + + wst->attached = TRUE; + return wst->frontBio; +} diff --git a/libfreerdp/core/gateway/wst.h b/libfreerdp/core/gateway/wst.h new file mode 100644 index 000000000..ecfeed664 --- /dev/null +++ b/libfreerdp/core/gateway/wst.h @@ -0,0 +1,39 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Websocket Transport + * + * Copyright 2023 Michael Saxl + * + * 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. + */ + +#ifndef FREERDP_LIB_CORE_GATEWAY_WEBSOCKET_TRANSPORT_H +#define FREERDP_LIB_CORE_GATEWAY_WEBSOCKET_TRANSPORT_H + +#include +#include + +/* needed for BIO */ +#include + +typedef struct rdp_wst rdpWst; + +FREERDP_LOCAL rdpWst* wst_new(rdpContext* context); +FREERDP_LOCAL void wst_free(rdpWst* wst); + +FREERDP_LOCAL BIO* wst_get_front_bio_and_take_ownership(rdpWst* wst); + +FREERDP_LOCAL BOOL wst_connect(rdpWst* wst, DWORD timeout); +FREERDP_LOCAL DWORD wst_get_event_handles(rdpWst* wst, HANDLE* events, DWORD count); + +#endif /* FREERDP_LIB_CORE_GATEWAY_WEBSOCKET_TRANSPORT_H */ diff --git a/libfreerdp/core/test/settings_property_lists.h b/libfreerdp/core/test/settings_property_lists.h index e61b954b4..1e2349b28 100644 --- a/libfreerdp/core/test/settings_property_lists.h +++ b/libfreerdp/core/test/settings_property_lists.h @@ -370,7 +370,9 @@ static const size_t string_list_indices[] = { FreeRDP_GatewayAccessToken, FreeRDP_GatewayDomain, FreeRDP_GatewayHostname, + FreeRDP_GatewayHttpExtAuthBearer, FreeRDP_GatewayPassword, + FreeRDP_GatewayUrl, FreeRDP_GatewayUsername, FreeRDP_HomePath, FreeRDP_ImeFileName, diff --git a/libfreerdp/core/transport.c b/libfreerdp/core/transport.c index 57681edc0..43de0ad74 100644 --- a/libfreerdp/core/transport.c +++ b/libfreerdp/core/transport.c @@ -54,6 +54,9 @@ #include "utils.h" #include "state.h" +#include "gateway/rdg.h" +#include "gateway/wst.h" + #define TAG FREERDP_TAG("core.transport") #define BUFFER_SIZE 16384 @@ -64,6 +67,7 @@ struct rdp_transport BIO* frontBio; rdpRdg* rdg; rdpTsg* tsg; + rdpWst* wst; rdpTls* tls; rdpContext* context; rdpNla* nla; @@ -463,8 +467,33 @@ BOOL transport_connect(rdpTransport* transport, const char* hostname, UINT16 por if (transport->GatewayEnabled) { + if (settings->GatewayUrl) + { + WINPR_ASSERT(!transport->wst); + transport->wst = wst_new(context); + + if (!transport->wst) + return FALSE; + + status = wst_connect(transport->wst, timeout); + + if (status) + { + transport->frontBio = wst_get_front_bio_and_take_ownership(transport->wst); + WINPR_ASSERT(transport->frontBio); + BIO_set_nonblock(transport->frontBio, 0); + transport->layer = TRANSPORT_LAYER_TSG; + status = TRUE; + } + else + { + wst_free(transport->wst); + transport->wst = NULL; + } + } if (!status && settings->GatewayHttpTransport) { + WINPR_ASSERT(!transport->rdg); transport->rdg = rdg_new(context); if (!transport->rdg) @@ -489,6 +518,7 @@ BOOL transport_connect(rdpTransport* transport, const char* hostname, UINT16 por if (!status && settings->GatewayRpcTransport && rpcFallback) { + WINPR_ASSERT(!transport->tsg); transport->tsg = tsg_new(transport); if (!transport->tsg) @@ -1230,6 +1260,16 @@ DWORD transport_get_event_handles(rdpTransport* transport, HANDLE* events, DWORD nCount += tmp; } + else if (transport->wst) + { + const DWORD tmp = + wst_get_event_handles(transport->wst, &events[nCount], count - nCount); + + if (tmp == 0) + return 0; + + nCount += tmp; + } } return nCount; diff --git a/libfreerdp/core/transport.h b/libfreerdp/core/transport.h index a40a89483..75813a0f9 100644 --- a/libfreerdp/core/transport.h +++ b/libfreerdp/core/transport.h @@ -34,7 +34,6 @@ typedef enum #include "rdstls.h" #include "gateway/tsg.h" -#include "gateway/rdg.h" #include #include