d55e035260
This PR creates a new /gateway:type:arm transport. It depends on CJSON The arm transport is not a transport by itself but is responsible for getting the websocket endpoint from a configuration url derived from the configured gateway host in the rdpw file or cmdline
556 lines
14 KiB
C
556 lines
14 KiB
C
/**
|
|
* FreeRDP: A Remote Desktop Protocol Implementation
|
|
* Websocket Framing
|
|
*
|
|
* Copyright 2023 Michael Saxl <mike@mwsys.mine.bz>
|
|
*
|
|
* 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 <freerdp/log.h>
|
|
#include "../tcp.h"
|
|
|
|
#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;
|
|
}
|
|
|
|
static int websocket_write_all(BIO* bio, const BYTE* data, size_t length)
|
|
{
|
|
WINPR_ASSERT(bio);
|
|
WINPR_ASSERT(data);
|
|
size_t offset = 0;
|
|
|
|
while (offset < length)
|
|
{
|
|
ERR_clear_error();
|
|
int status = BIO_write(bio, &data[offset], length - offset);
|
|
|
|
if (status > 0)
|
|
offset += status;
|
|
else
|
|
{
|
|
if (!BIO_should_retry(bio))
|
|
return -1;
|
|
|
|
if (BIO_write_blocked(bio))
|
|
status = BIO_wait_write(bio, 100);
|
|
else if (BIO_read_blocked(bio))
|
|
return -2; /* Abort write, there is data that must be read */
|
|
else
|
|
USleep(100);
|
|
|
|
if (status < 0)
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
return length;
|
|
}
|
|
|
|
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 = websocket_write_all(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 */
|
|
}
|