diff --git a/client/transports/Network/CMakeLists.txt b/client/transports/Network/CMakeLists.txt index d3f67016..055e27e5 100644 --- a/client/transports/Network/CMakeLists.txt +++ b/client/transports/Network/CMakeLists.txt @@ -7,6 +7,7 @@ endif() set(TRANSPORT_NETWORK_SOURCES audio.c + clipboard.c connection.c control.c cursor.c diff --git a/client/transports/Network/clipboard.c b/client/transports/Network/clipboard.c new file mode 100644 index 00000000..8214037e --- /dev/null +++ b/client/transports/Network/clipboard.c @@ -0,0 +1,4419 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "clipboard.h" + +#include "common/debug.h" +#include "common/locking.h" +#include "common/thread.h" +#include "common/time.h" + +#include +#include +#include + +#include +#include +#include + +#define NETWORK_CLIPBOARD_TIMER_MS 20U +#define NETWORK_CLIPBOARD_LEASE_MS 2000U +#define NETWORK_CLIPBOARD_KEEPALIVE_MS 750U +#define NETWORK_CLIPBOARD_CLAIM_RETRY_MS 500U +#define NETWORK_CLIPBOARD_PENDING_MAX 128U +#define NETWORK_CLIPBOARD_PENDING_BYTES (8U * 1024U * 1024U) +#define NETWORK_CLIPBOARD_INBOUND_MAX 128U +#define NETWORK_CLIPBOARD_INBOUND_BYTES (8U * 1024U * 1024U) +#define NETWORK_CLIPBOARD_PROCESS_MAX 64U +#define NETWORK_CLIPBOARD_EMITTED_CHUNK (256U * 1024U) +#define NETWORK_CLIPBOARD_FILE_READ_MAX (1024U * 1024U) +#define NETWORK_CLIPBOARD_FILE_ACQUISITIONS 8U +#define NETWORK_CLIPBOARD_FILE_REQUESTS 32U +#define NETWORK_CLIPBOARD_FORMATS \ + LG_NET_CLIPBOARD_KVMFR_FORMAT_COUNT + +#define NETWORK_CLIPBOARD_CLAIM_FLAGS \ + (LG_NET_CLIPBOARD_CLAIM_READ | LG_NET_CLIPBOARD_CLAIM_WRITE) + +struct NetworkClipboardSend +{ + struct NetworkClipboardSend * next; + LGNetService service; + uint16_t messageType; + uint16_t serviceVersion; + LGNetEnvelopeFlags flags; + NetworkPacketDelivery delivery; + uint64_t componentEpoch; + uint64_t requestID; + uint32_t payloadLength; + uint8_t payload[]; +}; + +struct NetworkClipboardBatch +{ + struct NetworkClipboardSend * head; + struct NetworkClipboardSend * tail; + unsigned count; + size_t bytes; +}; + +struct NetworkClipboardInbound +{ + struct NetworkClipboardInbound * next; + LGNetEnvelope envelope; + NetworkPacketDelivery delivery; + size_t payloadLength; + uint8_t payload[]; +}; + +struct NetworkClipboardTransfer +{ + struct NetworkClipboardTransfer * next; + LG_ClipboardRequest localRequest; + uint64_t wireRequest; + uint64_t offerID; + LG_ClipboardData type; + uint32_t formatIndex; + uint32_t maxChunkLength; + uint64_t sizeHint; + uint64_t offset; + NetworkPacketDelivery delivery; + bool began; + bool blocked; +}; + +struct NetworkFileTransfer +{ + struct NetworkFileTransfer * next; + LG_ClipboardFileRequest request; + uint64_t wireRequest; + uint64_t entryID; + uint64_t sizeHint; + uint64_t responseOffset; + NetworkPacketDelivery delivery; + bool began; + bool blocked; + bool wireEnded; +}; + +struct NetworkFileAcquisition +{ + struct NetworkFileAcquisition * next; + uint64_t dataset; + uint64_t localAcquisition; + uint64_t wireClientID; + uint64_t leaseEpoch; + bool fromRemote; + bool active; +}; + +struct NetworkRemoteOffer +{ + uint64_t epoch; + uint64_t offerID; + uint64_t serial; + uint32_t formatCount; + uint32_t received; + uint32_t formatMask; + LG_ClipboardData types[NETWORK_CLIPBOARD_FORMATS]; + uint32_t indices[LG_CLIPBOARD_DATA_NONE]; + bool complete; + bool published; + bool fileOffer; +}; + +struct NetworkLocalOffer +{ + uint64_t offerID; + uint64_t serial; + uint32_t count; + LG_ClipboardData types[NETWORK_CLIPBOARD_FORMATS]; + uint32_t indices[LG_CLIPBOARD_DATA_NONE]; + bool files; +}; + +struct NetworkClipboard +{ + NetworkConnection * connection; + LGTimer * timer; + LG_Lock stateLock; + LG_Lock eventLock; + LG_Lock statusLock; + + bool destroying; + bool connected; + bool clipboardService; + bool fileService; + bool clipboardBulk; + bool fileBulk; + bool available; + bool attached; + bool processing; + bool claimPending; + bool claimed; + bool inboundBlocked; + bool inboundReady; + bool sendProgress; + LG_ClipboardRequest inboundBlockedRequest; + uint16_t clipboardVersion; + uint16_t fileVersion; + uint32_t statusGeneration; + bool statusNotifyPending; + + uint64_t ownerID; + uint64_t claimEpoch; + uint64_t claimSerial; + uint64_t lastClaimStatusSerial; + uint64_t nextClaimNs; + uint64_t nextKeepaliveNs; + uint64_t localSerial; + uint64_t wireSerial; + uint64_t remoteSerial; + + struct NetworkClipboardSend * sendHead; + struct NetworkClipboardSend * sendTail; + unsigned sendCount; + size_t sendBytes; + + struct NetworkClipboardInbound * inboundHead; + struct NetworkClipboardInbound * inboundTail; + unsigned inboundCount; + size_t inboundBytes; + + struct NetworkClipboardTransfer * clipboardReads; + struct NetworkClipboardTransfer * clipboardWrites; + struct NetworkFileTransfer * fileReads; + struct NetworkFileTransfer * fileWrites; + struct NetworkFileAcquisition * fileAcquisitions; + + struct NetworkRemoteOffer remoteOffer; + struct NetworkLocalOffer localOffer; + + const LG_ClipboardEventOps * events; + void * eventOpaque; + LG_ClipboardStatusFn statusCallback; + void * statusOpaque; +}; + +static uint64_t networkClipboardNext64(uint64_t value) +{ + ++value; + return value ? value : 1U; +} + +static uint32_t networkClipboardNext32(uint32_t value) +{ + ++value; + return value ? value : 1U; +} + +static uint64_t networkClipboardDeadline(uint32_t milliseconds) +{ + return nanotime() + (uint64_t)milliseconds * UINT64_C(1000000); +} + +static uint64_t networkClipboardNextLocalIDNL( + NetworkClipboard * clipboard, bool remote) +{ + clipboard->localSerial = networkClipboardNext64(clipboard->localSerial); + clipboard->localSerial &= ~KVMFR_CLIPBOARD_TRANSFER_HELPER; + if (!clipboard->localSerial) + clipboard->localSerial = 1; + + return remote ? clipboard->localSerial | + KVMFR_CLIPBOARD_TRANSFER_HELPER : clipboard->localSerial; +} + +static uint64_t networkClipboardNextWireIDNL( + NetworkClipboard * clipboard) +{ + clipboard->wireSerial = networkClipboardNext64(clipboard->wireSerial); + return clipboard->wireSerial; +} + +static LG_ClipboardData networkClipboardDataFromFormat( + KVMFRClipboardFormat format) +{ + switch (format) + { + case KVMFR_CLIPBOARD_FORMAT_TEXT : return LG_CLIPBOARD_DATA_TEXT; + case KVMFR_CLIPBOARD_FORMAT_PNG : return LG_CLIPBOARD_DATA_PNG; + case KVMFR_CLIPBOARD_FORMAT_BMP : return LG_CLIPBOARD_DATA_BMP; + case KVMFR_CLIPBOARD_FORMAT_TIFF : return LG_CLIPBOARD_DATA_TIFF; + case KVMFR_CLIPBOARD_FORMAT_JPEG : return LG_CLIPBOARD_DATA_JPEG; + case KVMFR_CLIPBOARD_FORMAT_FILES: return LG_CLIPBOARD_DATA_FILES; + default: return LG_CLIPBOARD_DATA_NONE; + } +} + +static KVMFRClipboardFormat networkClipboardFormatFromData( + LG_ClipboardData type) +{ + switch (type) + { + case LG_CLIPBOARD_DATA_TEXT : return KVMFR_CLIPBOARD_FORMAT_TEXT; + case LG_CLIPBOARD_DATA_PNG : return KVMFR_CLIPBOARD_FORMAT_PNG; + case LG_CLIPBOARD_DATA_BMP : return KVMFR_CLIPBOARD_FORMAT_BMP; + case LG_CLIPBOARD_DATA_TIFF : return KVMFR_CLIPBOARD_FORMAT_TIFF; + case LG_CLIPBOARD_DATA_JPEG : return KVMFR_CLIPBOARD_FORMAT_JPEG; + case LG_CLIPBOARD_DATA_FILES: return KVMFR_CLIPBOARD_FORMAT_FILES; + default: return KVMFR_CLIPBOARD_FORMAT_NONE; + } +} + +static bool networkClipboardDataValid(LG_ClipboardData type) +{ + return type >= LG_CLIPBOARD_DATA_TEXT && + type < LG_CLIPBOARD_DATA_NONE; +} + +static bool networkClipboardFileErrorValid(LG_ClipboardFileError error) +{ + return error >= LG_CLIPBOARD_FILE_ERROR_NONE && + error <= LG_CLIPBOARD_FILE_ERROR_STALE; +} + +static KVMFRClipboardFileError networkClipboardFileErrorToKVMFR( + LG_ClipboardFileError error) +{ + return networkClipboardFileErrorValid(error) ? + (KVMFRClipboardFileError)error : + KVMFR_CLIPBOARD_FILE_ERROR_INVALID; +} + +static LG_ClipboardFileError networkClipboardFileErrorFromStatus( + LGNetFileStatusCode status, uint32_t detail) +{ + return (LG_ClipboardFileError)lgNetFileKVMFRErrorFromStatus( + status, detail); +} + +static LGNetFileStatusCode networkClipboardFileStatus( + LG_ClipboardFileError error) +{ + return lgNetFileStatusFromKVMFRError( + networkClipboardFileErrorToKVMFR(error)); +} + +static LG_ClipboardStatus networkClipboardStatusNL( + const NetworkClipboard * clipboard) +{ + return (LG_ClipboardStatus) + { + .available = clipboard->available, + .generation = clipboard->statusGeneration, + }; +} + +static void networkClipboardSetAvailableNL(NetworkClipboard * clipboard, + bool available) +{ + if (clipboard->available == available) + return; + + clipboard->available = available; + clipboard->statusGeneration = + networkClipboardNext32(clipboard->statusGeneration); + clipboard->statusNotifyPending = true; +} + +static void networkClipboardNotifyStatus(NetworkClipboard * clipboard) +{ + LG_ClipboardStatusFn callback; + void * opaque; + LG_ClipboardStatus status; + + LG_LOCK(clipboard->statusLock); + LG_LOCK(clipboard->stateLock); + if (!clipboard->statusNotifyPending) + { + LG_UNLOCK(clipboard->stateLock); + LG_UNLOCK(clipboard->statusLock); + return; + } + + clipboard->statusNotifyPending = false; + callback = clipboard->statusCallback; + opaque = clipboard->statusOpaque; + status = networkClipboardStatusNL(clipboard); + LG_UNLOCK(clipboard->stateLock); + + if (callback) + callback(opaque, &status); + LG_UNLOCK(clipboard->statusLock); +} + +static struct NetworkClipboardSend * networkClipboardSendNew( + LGNetService service, uint16_t messageType, uint16_t serviceVersion, + LGNetEnvelopeFlags flags, uint64_t componentEpoch, uint64_t requestID, + uint32_t payloadLength) +{ + const size_t allocation = sizeof(struct NetworkClipboardSend) + + (size_t)payloadLength; + if (payloadLength > LG_NET_MAX_PAYLOAD_LENGTH || + allocation < payloadLength) + return NULL; + + struct NetworkClipboardSend * send = + calloc(1, allocation); + if (!send) + return NULL; + + send->service = service; + send->messageType = messageType; + send->serviceVersion = serviceVersion; + send->flags = flags | LG_NET_ENVELOPE_FINAL; + send->delivery = NETWORK_PACKET_DELIVERY_RELIABLE; + send->componentEpoch = componentEpoch; + send->requestID = requestID; + send->payloadLength = payloadLength; + return send; +} + +static struct NetworkClipboardSend * networkClipboardSendWithDelivery( + struct NetworkClipboardSend * send, NetworkPacketDelivery delivery) +{ + if (send) + send->delivery = delivery; + return send; +} + +static void networkClipboardBatchAppend( + struct NetworkClipboardBatch * batch, + struct NetworkClipboardSend * send) +{ + if (batch->tail) + batch->tail->next = send; + else + batch->head = send; + batch->tail = send; + ++batch->count; + batch->bytes += sizeof(*send) + send->payloadLength; +} + +static void networkClipboardBatchFree(struct NetworkClipboardBatch * batch) +{ + while (batch->head) + { + struct NetworkClipboardSend * send = batch->head; + batch->head = send->next; + free(send); + } + memset(batch, 0, sizeof(*batch)); +} + +static bool networkClipboardQueueBatchNL(NetworkClipboard * clipboard, + struct NetworkClipboardBatch * batch) +{ + if (!batch->head || !clipboard->connected || + batch->count > NETWORK_CLIPBOARD_PENDING_MAX || + batch->bytes > NETWORK_CLIPBOARD_PENDING_BYTES || + clipboard->sendCount > NETWORK_CLIPBOARD_PENDING_MAX - batch->count || + clipboard->sendBytes > NETWORK_CLIPBOARD_PENDING_BYTES - batch->bytes) + return false; + + if (batch->head->service == LG_NET_SERVICE_CLIPBOARD && + !clipboard->clipboardService) + return false; + if (batch->head->service == LG_NET_SERVICE_FILE && + !clipboard->fileService) + return false; + + if (clipboard->sendTail) + clipboard->sendTail->next = batch->head; + else + clipboard->sendHead = batch->head; + clipboard->sendTail = batch->tail; + clipboard->sendCount += batch->count; + clipboard->sendBytes += batch->bytes; + memset(batch, 0, sizeof(*batch)); + return true; +} + +static void networkClipboardClearSendNL(NetworkClipboard * clipboard) +{ + while (clipboard->sendHead) + { + struct NetworkClipboardSend * send = clipboard->sendHead; + clipboard->sendHead = send->next; + free(send); + } + + clipboard->sendTail = NULL; + clipboard->sendCount = 0; + clipboard->sendBytes = 0; + clipboard->sendProgress = false; +} + +static bool networkClipboardFlushNL(NetworkClipboard * clipboard) +{ + bool progress = false; + uint8_t blocked = 0; + while (clipboard->sendHead && clipboard->connected && + blocked != ((UINT8_C(1) << NETWORK_PACKET_DELIVERY_RELIABLE) | + (UINT8_C(1) << NETWORK_PACKET_DELIVERY_BULK))) + { + struct NetworkClipboardSend ** link = &clipboard->sendHead; + while (*link && (blocked & (UINT8_C(1) << (*link)->delivery))) + link = &(*link)->next; + if (!*link) + break; + + struct NetworkClipboardSend * send = *link; + LG_TransportStatus status; + switch (send->delivery) + { + case NETWORK_PACKET_DELIVERY_RELIABLE: + status = networkConnectionSend( + clipboard->connection, send->service, send->messageType, + send->serviceVersion, send->flags, send->componentEpoch, + send->requestID, send->payload, send->payloadLength); + break; + + case NETWORK_PACKET_DELIVERY_BULK: + status = networkConnectionSendBulk( + clipboard->connection, send->service, send->messageType, + send->serviceVersion, send->flags, send->componentEpoch, + send->requestID, send->payload, send->payloadLength); + break; + + case NETWORK_PACKET_DELIVERY_DATAGRAM: + default: + return progress; + } + if (status == LG_TRANSPORT_UNAVAILABLE) + { + blocked |= UINT8_C(1) << send->delivery; + continue; + } + if (status != LG_TRANSPORT_OK) + break; + + *link = send->next; + if (clipboard->sendTail == send) + { + clipboard->sendTail = NULL; + for (struct NetworkClipboardSend * item = clipboard->sendHead; + item; item = item->next) + clipboard->sendTail = item; + } + --clipboard->sendCount; + clipboard->sendBytes -= sizeof(*send) + send->payloadLength; + free(send); + progress = true; + } + return progress; +} + +static void networkClipboardFlushQueued(NetworkClipboard * clipboard) +{ + LG_LOCK(clipboard->stateLock); + if (!clipboard->destroying) + clipboard->sendProgress |= networkClipboardFlushNL(clipboard); + LG_UNLOCK(clipboard->stateLock); +} + +static void networkClipboardClearInboundNL(NetworkClipboard * clipboard) +{ + while (clipboard->inboundHead) + { + struct NetworkClipboardInbound * packet = clipboard->inboundHead; + clipboard->inboundHead = packet->next; + free(packet); + } + + clipboard->inboundTail = NULL; + clipboard->inboundCount = 0; + clipboard->inboundBytes = 0; + clipboard->inboundBlocked = false; + clipboard->inboundReady = false; + clipboard->inboundBlockedRequest = LG_CLIPBOARD_REQUEST_INVALID; +} + +static struct NetworkClipboardTransfer * networkClipboardTransferFind( + struct NetworkClipboardTransfer * transfers, uint64_t localRequest) +{ + for (; transfers; transfers = transfers->next) + if (transfers->localRequest == localRequest) + return transfers; + return NULL; +} + +static struct NetworkClipboardTransfer * networkClipboardTransferFindWire( + struct NetworkClipboardTransfer * transfers, uint64_t wireRequest) +{ + for (; transfers; transfers = transfers->next) + if (transfers->wireRequest == wireRequest) + return transfers; + return NULL; +} + +static struct NetworkClipboardTransfer * networkClipboardTransferTake( + struct NetworkClipboardTransfer ** transfers, uint64_t localRequest) +{ + while (*transfers && (*transfers)->localRequest != localRequest) + transfers = &(*transfers)->next; + if (!*transfers) + return NULL; + + struct NetworkClipboardTransfer * result = *transfers; + *transfers = result->next; + result->next = NULL; + return result; +} + +static struct NetworkClipboardTransfer * networkClipboardTransferTakeWire( + struct NetworkClipboardTransfer ** transfers, uint64_t wireRequest) +{ + while (*transfers && (*transfers)->wireRequest != wireRequest) + transfers = &(*transfers)->next; + if (!*transfers) + return NULL; + + struct NetworkClipboardTransfer * result = *transfers; + *transfers = result->next; + result->next = NULL; + return result; +} + +static struct NetworkFileTransfer * networkClipboardFileTransferFind( + struct NetworkFileTransfer * transfers, uint64_t localRequest) +{ + for (; transfers; transfers = transfers->next) + if (transfers->request.request == localRequest) + return transfers; + return NULL; +} + +static struct NetworkFileTransfer * networkClipboardFileTransferFindWire( + struct NetworkFileTransfer * transfers, uint64_t wireRequest) +{ + for (; transfers; transfers = transfers->next) + if (transfers->wireRequest == wireRequest) + return transfers; + return NULL; +} + +static struct NetworkFileTransfer * networkClipboardFileTransferTake( + struct NetworkFileTransfer ** transfers, uint64_t localRequest) +{ + while (*transfers && (*transfers)->request.request != localRequest) + transfers = &(*transfers)->next; + if (!*transfers) + return NULL; + + struct NetworkFileTransfer * result = *transfers; + *transfers = result->next; + result->next = NULL; + return result; +} + +static struct NetworkFileTransfer * networkClipboardFileTransferTakeWire( + struct NetworkFileTransfer ** transfers, uint64_t wireRequest) +{ + while (*transfers && (*transfers)->wireRequest != wireRequest) + transfers = &(*transfers)->next; + if (!*transfers) + return NULL; + + struct NetworkFileTransfer * result = *transfers; + *transfers = result->next; + result->next = NULL; + return result; +} + +static struct NetworkFileAcquisition * networkClipboardAcquisitionFind( + NetworkClipboard * clipboard, uint64_t dataset, + uint64_t localAcquisition, bool fromRemote) +{ + for (struct NetworkFileAcquisition * item = clipboard->fileAcquisitions; + item; item = item->next) + if (item->dataset == dataset && + item->localAcquisition == localAcquisition && + item->fromRemote == fromRemote) + return item; + return NULL; +} + +static struct NetworkFileAcquisition * +networkClipboardAcquisitionFindWire(NetworkClipboard * clipboard, + uint64_t dataset, uint64_t wireClientID, bool fromRemote) +{ + for (struct NetworkFileAcquisition * item = clipboard->fileAcquisitions; + item; item = item->next) + if (item->dataset == dataset && item->wireClientID == wireClientID && + item->fromRemote == fromRemote) + return item; + return NULL; +} + +static struct NetworkFileAcquisition * networkClipboardAcquisitionTake( + NetworkClipboard * clipboard, uint64_t dataset, + uint64_t localAcquisition, bool fromRemote) +{ + struct NetworkFileAcquisition ** item = &clipboard->fileAcquisitions; + while (*item && ((*item)->dataset != dataset || + (*item)->localAcquisition != localAcquisition || + (*item)->fromRemote != fromRemote)) + item = &(*item)->next; + if (!*item) + return NULL; + + struct NetworkFileAcquisition * result = *item; + *item = result->next; + result->next = NULL; + return result; +} + +static unsigned networkClipboardAcquisitionCountNL( + const NetworkClipboard * clipboard) +{ + unsigned count = 0; + for (const struct NetworkFileAcquisition * item = + clipboard->fileAcquisitions; item; item = item->next) + ++count; + return count; +} + +static unsigned networkClipboardFileTransferCountNL( + const NetworkClipboard * clipboard) +{ + unsigned count = 0; + for (const struct NetworkFileTransfer * item = clipboard->fileReads; + item; item = item->next) + ++count; + for (const struct NetworkFileTransfer * item = clipboard->fileWrites; + item; item = item->next) + ++count; + return count; +} + +static unsigned networkClipboardTransferCount( + const struct NetworkClipboardTransfer * transfers) +{ + unsigned count = 0; + for (; transfers; transfers = transfers->next) + ++count; + return count; +} + +static bool networkClipboardDatasetAcquiredNL( + const NetworkClipboard * clipboard, uint64_t dataset, + bool fromRemote) +{ + for (const struct NetworkFileAcquisition * item = + clipboard->fileAcquisitions; item; item = item->next) + if (item->dataset == dataset && item->fromRemote == fromRemote && + item->active) + return true; + return false; +} + +static void networkClipboardTransfersFree( + struct NetworkClipboardTransfer * transfers) +{ + while (transfers) + { + struct NetworkClipboardTransfer * next = transfers->next; + free(transfers); + transfers = next; + } +} + +static void networkClipboardFileTransfersFree( + struct NetworkFileTransfer * transfers) +{ + while (transfers) + { + struct NetworkFileTransfer * next = transfers->next; + free(transfers); + transfers = next; + } +} + +static void networkClipboardAcquisitionsFree( + struct NetworkFileAcquisition * acquisitions) +{ + while (acquisitions) + { + struct NetworkFileAcquisition * next = acquisitions->next; + free(acquisitions); + acquisitions = next; + } +} + +static bool networkClipboardSameFileRequest( + const LG_ClipboardFileRequest * left, + const LG_ClipboardFileRequest * right) +{ + return left->dataset == right->dataset && + left->request == right->request && left->node == right->node && + left->offset == right->offset && left->length == right->length && + left->operation == right->operation; +} + +static struct NetworkClipboardSend * networkClipboardControlSendNew( + NetworkClipboard * clipboard, uint16_t messageType, + const LGNetClipboardControl * control, LGNetEnvelopeFlags flags, + uint64_t requestID) +{ + struct NetworkClipboardSend * send = networkClipboardSendNew( + LG_NET_SERVICE_CLIPBOARD, messageType, clipboard->clipboardVersion, + flags, clipboard->claimEpoch, requestID, + LG_NET_CLIPBOARD_CONTROL_WIRE_SIZE); + if (!send || !lgNetClipboardControlEncode( + send->payload, send->payloadLength, control)) + { + free(send); + return NULL; + } + return send; +} + +static struct NetworkClipboardSend * networkClipboardTransferSendNew( + NetworkClipboard * clipboard, uint16_t messageType, + const LGNetClipboardTransfer * transfer, LGNetEnvelopeFlags flags) +{ + struct NetworkClipboardSend * send = networkClipboardSendNew( + LG_NET_SERVICE_CLIPBOARD, messageType, clipboard->clipboardVersion, + flags, clipboard->claimEpoch, transfer->requestID, + LG_NET_CLIPBOARD_TRANSFER_WIRE_SIZE); + if (!send || !lgNetClipboardTransferEncode( + send->payload, send->payloadLength, transfer)) + { + free(send); + return NULL; + } + return send; +} + +static struct NetworkClipboardSend * networkClipboardChunkSendNew( + NetworkClipboard * clipboard, const LGNetClipboardChunk * chunk) +{ + const size_t payloadLength = lgNetClipboardChunkSize(chunk); + if (!payloadLength || payloadLength > UINT32_MAX) + return NULL; + + struct NetworkClipboardSend * send = networkClipboardSendNew( + LG_NET_SERVICE_CLIPBOARD, LG_NET_CLIPBOARD_MESSAGE_DATA_CHUNK, + clipboard->clipboardVersion, 0, clipboard->claimEpoch, + chunk->requestID, (uint32_t)payloadLength); + if (!send || !lgNetClipboardChunkEncode( + send->payload, send->payloadLength, chunk)) + { + free(send); + return NULL; + } + return send; +} + +static struct NetworkClipboardSend * networkClipboardStatusSendNew( + NetworkClipboard * clipboard, const LGNetClipboardStatus * status, + LGNetEnvelopeFlags flags) +{ + struct NetworkClipboardSend * send = networkClipboardSendNew( + LG_NET_SERVICE_CLIPBOARD, LG_NET_CLIPBOARD_MESSAGE_STATUS, + clipboard->clipboardVersion, flags, clipboard->claimEpoch, + status->requestID, LG_NET_CLIPBOARD_STATUS_WIRE_SIZE); + if (!send || !lgNetClipboardStatusEncode( + send->payload, send->payloadLength, status)) + { + free(send); + return NULL; + } + return send; +} + +static struct NetworkClipboardSend * networkClipboardFileLeaseSendNew( + NetworkClipboard * clipboard, uint16_t messageType, + const LGNetFileLease * lease, LGNetEnvelopeFlags flags) +{ + struct NetworkClipboardSend * send = networkClipboardSendNew( + LG_NET_SERVICE_FILE, messageType, clipboard->fileVersion, flags, + clipboard->claimEpoch, lease->clientID, LG_NET_FILE_LEASE_WIRE_SIZE); + if (!send || !lgNetFileLeaseEncode( + send->payload, send->payloadLength, lease)) + { + free(send); + return NULL; + } + return send; +} + +static struct NetworkClipboardSend * networkClipboardFileTransferSendNew( + NetworkClipboard * clipboard, uint16_t messageType, + const LGNetFileTransfer * transfer, LGNetEnvelopeFlags flags) +{ + struct NetworkClipboardSend * send = networkClipboardSendNew( + LG_NET_SERVICE_FILE, messageType, clipboard->fileVersion, flags, + clipboard->claimEpoch, transfer->requestID, + LG_NET_FILE_TRANSFER_WIRE_SIZE); + if (!send || !lgNetFileTransferEncode( + send->payload, send->payloadLength, transfer)) + { + free(send); + return NULL; + } + return send; +} + +static struct NetworkClipboardSend * networkClipboardFileChunkSendNew( + NetworkClipboard * clipboard, const LGNetFileChunk * chunk) +{ + const size_t payloadLength = lgNetFileChunkSize(chunk); + if (!payloadLength || payloadLength > UINT32_MAX) + return NULL; + + struct NetworkClipboardSend * send = networkClipboardSendNew( + LG_NET_SERVICE_FILE, LG_NET_FILE_MESSAGE_DATA_CHUNK, + clipboard->fileVersion, 0, clipboard->claimEpoch, + chunk->requestID, (uint32_t)payloadLength); + if (!send || !lgNetFileChunkEncode( + send->payload, send->payloadLength, chunk)) + { + free(send); + return NULL; + } + return send; +} + +static struct NetworkClipboardSend * networkClipboardFileStatusSendNew( + NetworkClipboard * clipboard, const LGNetFileStatus * status, + LGNetEnvelopeFlags flags) +{ + struct NetworkClipboardSend * send = networkClipboardSendNew( + LG_NET_SERVICE_FILE, LG_NET_FILE_MESSAGE_STATUS, + clipboard->fileVersion, flags, clipboard->claimEpoch, + status->requestID, LG_NET_FILE_STATUS_WIRE_SIZE); + if (!send || !lgNetFileStatusEncode( + send->payload, send->payloadLength, status)) + { + free(send); + return NULL; + } + return send; +} + +static struct NetworkClipboardSend * networkClipboardOfferSendNew( + NetworkClipboard * clipboard, uint64_t offerID, uint64_t serial, + uint32_t formatIndex, uint32_t formatCount, LG_ClipboardData type) +{ + const KVMFRClipboardFormat format = + networkClipboardFormatFromData(type); + const uint8_t * mime; + uint16_t mimeLength; + if (!lgNetClipboardKVMFRFormatMime(format, &mime, &mimeLength)) + return NULL; + + const LGNetClipboardOffer offer = + { + .offerID = offerID, + .serial = serial, + .estimatedSize = 0, + .formatIndex = formatIndex, + .formatCount = formatCount, + .mimeLength = mimeLength, + .flags = LG_NET_CLIPBOARD_OFFER_LAZY, + .mime = mime, + }; + const size_t payloadLength = lgNetClipboardOfferSize(&offer); + if (!payloadLength || payloadLength > UINT32_MAX) + return NULL; + + struct NetworkClipboardSend * send = networkClipboardSendNew( + LG_NET_SERVICE_CLIPBOARD, LG_NET_CLIPBOARD_MESSAGE_OFFER, + clipboard->clipboardVersion, 0, clipboard->claimEpoch, offerID, + (uint32_t)payloadLength); + if (!send || !lgNetClipboardOfferEncode( + send->payload, send->payloadLength, &offer)) + { + free(send); + return NULL; + } + return send; +} + +static struct NetworkClipboardSend * networkClipboardRequestSendNew( + NetworkClipboard * clipboard, const LGNetClipboardRequest * request) +{ + struct NetworkClipboardSend * send = networkClipboardSendNew( + LG_NET_SERVICE_CLIPBOARD, LG_NET_CLIPBOARD_MESSAGE_REQUEST, + clipboard->clipboardVersion, LG_NET_ENVELOPE_ACK_REQUIRED, + clipboard->claimEpoch, request->requestID, + LG_NET_CLIPBOARD_REQUEST_WIRE_SIZE); + if (!send || !lgNetClipboardRequestEncode( + send->payload, send->payloadLength, request)) + { + free(send); + return NULL; + } + return send; +} + +static struct NetworkClipboardSend * networkClipboardFileOfferSendNew( + NetworkClipboard * clipboard, uint64_t offerID) +{ + const LGNetFileOffer offer = + { + .offerID = offerID, + .totalBytes = 0, + .entryCount = 1, + .flags = LG_NET_FILE_OFFER_COPY | + LG_NET_FILE_OFFER_RECURSIVE, + .labelLength = 0, + .label = NULL, + }; + const size_t payloadLength = lgNetFileOfferSize(&offer); + if (!payloadLength || payloadLength > UINT32_MAX) + return NULL; + + struct NetworkClipboardSend * send = networkClipboardSendNew( + LG_NET_SERVICE_FILE, LG_NET_FILE_MESSAGE_OFFER, + clipboard->fileVersion, 0, clipboard->claimEpoch, offerID, + (uint32_t)payloadLength); + if (!send || !lgNetFileOfferEncode( + send->payload, send->payloadLength, &offer)) + { + free(send); + return NULL; + } + return send; +} + +static struct NetworkClipboardSend * networkClipboardFileRequestSendNew( + NetworkClipboard * clipboard, const LGNetFileRequest * request) +{ + struct NetworkClipboardSend * send = networkClipboardSendNew( + LG_NET_SERVICE_FILE, LG_NET_FILE_MESSAGE_REQUEST, + clipboard->fileVersion, LG_NET_ENVELOPE_ACK_REQUIRED, + clipboard->claimEpoch, request->requestID, + LG_NET_FILE_REQUEST_WIRE_SIZE); + if (!send || !lgNetFileRequestEncode( + send->payload, send->payloadLength, request)) + { + free(send); + return NULL; + } + return send; +} + +static bool networkClipboardQueueClaimNL(NetworkClipboard * clipboard) +{ + if (!clipboard->connected || !clipboard->clipboardService || + clipboard->destroying || !clipboard->attached || + clipboard->claimPending || clipboard->claimed) + return false; + + const uint64_t claimEpoch = networkClipboardNext64( + clipboard->claimEpoch); + const LGNetClipboardClaim claim = + { + .ownerID = clipboard->ownerID, + .claimEpoch = claimEpoch, + .leaseMs = NETWORK_CLIPBOARD_LEASE_MS, + .flags = NETWORK_CLIPBOARD_CLAIM_FLAGS, + }; + struct NetworkClipboardSend * send = networkClipboardSendNew( + LG_NET_SERVICE_CLIPBOARD, LG_NET_CLIPBOARD_MESSAGE_CLAIM, + clipboard->clipboardVersion, LG_NET_ENVELOPE_ACK_REQUIRED, + claimEpoch, claimEpoch, LG_NET_CLIPBOARD_CLAIM_WIRE_SIZE); + if (!send || !lgNetClipboardClaimEncode( + send->payload, send->payloadLength, &claim)) + { + free(send); + return false; + } + + struct NetworkClipboardBatch batch = { 0 }; + networkClipboardBatchAppend(&batch, send); + if (!networkClipboardQueueBatchNL(clipboard, &batch)) + { + networkClipboardBatchFree(&batch); + return false; + } + + clipboard->claimEpoch = claimEpoch; + clipboard->claimPending = true; + clipboard->claimed = false; + clipboard->claimSerial = 0; + clipboard->lastClaimStatusSerial = 0; + clipboard->nextClaimNs = networkClipboardDeadline( + NETWORK_CLIPBOARD_CLAIM_RETRY_MS); + clipboard->nextKeepaliveNs = 0; + return true; +} + +static bool networkClipboardQueueControlNL(NetworkClipboard * clipboard, + uint16_t messageType, uint64_t offerID) +{ + const uint64_t serial = + networkClipboardNext64(clipboard->claimSerial); + const LGNetClipboardControl control = + { + .ownerID = clipboard->ownerID, + .claimEpoch = clipboard->claimEpoch, + .serial = serial, + .offerID = offerID, + }; + struct NetworkClipboardSend * send = networkClipboardControlSendNew( + clipboard, messageType, &control, 0, offerID); + if (!send) + return false; + + struct NetworkClipboardBatch batch = { 0 }; + networkClipboardBatchAppend(&batch, send); + if (!networkClipboardQueueBatchNL(clipboard, &batch)) + { + networkClipboardBatchFree(&batch); + return false; + } + + clipboard->claimSerial = serial; + return true; +} + +static bool networkClipboardQueueReleaseNL(NetworkClipboard * clipboard) +{ + if ((!clipboard->claimPending && !clipboard->claimed) || + !clipboard->connected || !clipboard->clipboardService) + return false; + + const bool queued = networkClipboardQueueControlNL(clipboard, + LG_NET_CLIPBOARD_MESSAGE_RELEASE, 0); + clipboard->claimPending = false; + clipboard->claimed = false; + clipboard->nextClaimNs = 0; + clipboard->nextKeepaliveNs = 0; + return queued; +} + +static bool networkClipboardQueueLocalOfferNL( + NetworkClipboard * clipboard) +{ + if (!clipboard->localOffer.offerID || !clipboard->localOffer.count || + (!clipboard->claimPending && !clipboard->claimed)) + return false; + + struct NetworkClipboardBatch batch = { 0 }; + if (clipboard->localOffer.files) + { + struct NetworkClipboardSend * file = networkClipboardFileOfferSendNew( + clipboard, clipboard->localOffer.offerID); + if (!file) + return false; + networkClipboardBatchAppend(&batch, file); + } + + for (uint32_t index = 0; index < clipboard->localOffer.count; ++index) + { + struct NetworkClipboardSend * offer = networkClipboardOfferSendNew( + clipboard, clipboard->localOffer.offerID, + clipboard->localOffer.serial, index, + clipboard->localOffer.count, clipboard->localOffer.types[index]); + if (!offer) + { + networkClipboardBatchFree(&batch); + return false; + } + networkClipboardBatchAppend(&batch, offer); + } + + if (!networkClipboardQueueBatchNL(clipboard, &batch)) + { + networkClipboardBatchFree(&batch); + return false; + } + return true; +} + +static void networkClipboardDispatchNotice(NetworkClipboard * clipboard, + uint64_t dataset, const LG_ClipboardData types[], size_t count, + bool files) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->stateLock); + const LG_ClipboardEventOps * events = clipboard->attached ? + clipboard->events : NULL; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->stateLock); + + if (events) + { + if (files && events->fileOffer) + events->fileOffer(opaque, dataset); + if (events->notice) + events->notice(opaque, types, count); + } + LG_UNLOCK(clipboard->eventLock); +} + +static void networkClipboardDispatchRelease(NetworkClipboard * clipboard) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->stateLock); + const LG_ClipboardEventOps * events = clipboard->attached ? + clipboard->events : NULL; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->stateLock); + + if (events && events->release) + events->release(opaque); + LG_UNLOCK(clipboard->eventLock); +} + +static bool networkClipboardDispatchRequest(NetworkClipboard * clipboard, + LG_ClipboardRequest request, LG_ClipboardData type) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->stateLock); + const LG_ClipboardEventOps * events = clipboard->attached ? + clipboard->events : NULL; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->stateLock); + + const bool accepted = events && events->request && + events->request(opaque, request, type); + LG_UNLOCK(clipboard->eventLock); + return accepted; +} + +static LG_ClipboardResult networkClipboardDispatchDataBegin( + NetworkClipboard * clipboard, LG_ClipboardRequest request, + LG_ClipboardData type, uint64_t sizeHint) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->stateLock); + const LG_ClipboardEventOps * events = clipboard->attached ? + clipboard->events : NULL; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->stateLock); + + const LG_ClipboardResult result = events && events->dataBegin ? + events->dataBegin(opaque, request, type, sizeHint) : + LG_CLIPBOARD_RESULT_FAILED; + LG_UNLOCK(clipboard->eventLock); + return result; +} + +static LG_ClipboardResult networkClipboardDispatchDataChunk( + NetworkClipboard * clipboard, LG_ClipboardRequest request, + uint64_t offset, const void * data, size_t size) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->stateLock); + const LG_ClipboardEventOps * events = clipboard->attached ? + clipboard->events : NULL; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->stateLock); + + const LG_ClipboardResult result = events && events->dataChunk ? + events->dataChunk(opaque, request, offset, data, size) : + LG_CLIPBOARD_RESULT_FAILED; + LG_UNLOCK(clipboard->eventLock); + return result; +} + +static LG_ClipboardResult networkClipboardDispatchDataEnd( + NetworkClipboard * clipboard, LG_ClipboardRequest request, + uint64_t finalSize) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->stateLock); + const LG_ClipboardEventOps * events = clipboard->attached ? + clipboard->events : NULL; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->stateLock); + + const LG_ClipboardResult result = events && events->dataEnd ? + events->dataEnd(opaque, request, finalSize) : + LG_CLIPBOARD_RESULT_FAILED; + LG_UNLOCK(clipboard->eventLock); + return result; +} + +static void networkClipboardDispatchDataCancel( + NetworkClipboard * clipboard, LG_ClipboardRequest request, + LG_ClipboardCancelReason reason) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->stateLock); + const LG_ClipboardEventOps * events = clipboard->attached ? + clipboard->events : NULL; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->stateLock); + + if (events && events->dataCancel) + events->dataCancel(opaque, request, reason); + LG_UNLOCK(clipboard->eventLock); +} + +static void networkClipboardDispatchRequestCancel( + NetworkClipboard * clipboard, LG_ClipboardRequest request, + LG_ClipboardCancelReason reason) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->stateLock); + const LG_ClipboardEventOps * events = clipboard->attached ? + clipboard->events : NULL; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->stateLock); + + if (events && events->requestCancel) + events->requestCancel(opaque, request, reason); + LG_UNLOCK(clipboard->eventLock); +} + +static void networkClipboardDispatchDataReady( + NetworkClipboard * clipboard, LG_ClipboardRequest request) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->stateLock); + const LG_ClipboardEventOps * events = clipboard->attached ? + clipboard->events : NULL; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->stateLock); + + if (events && events->dataReady) + events->dataReady(opaque, request); + LG_UNLOCK(clipboard->eventLock); +} + +static void networkClipboardDispatchFileAcquire( + NetworkClipboard * clipboard, uint64_t dataset, uint64_t acquisition) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->stateLock); + const LG_ClipboardEventOps * events = clipboard->attached ? + clipboard->events : NULL; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->stateLock); + + if (events && events->fileAcquire) + events->fileAcquire(opaque, dataset, acquisition); + LG_UNLOCK(clipboard->eventLock); +} + +static void networkClipboardDispatchFileAcquired( + NetworkClipboard * clipboard, uint64_t dataset, uint64_t acquisition, + LG_ClipboardFileError error) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->stateLock); + const LG_ClipboardEventOps * events = clipboard->attached ? + clipboard->events : NULL; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->stateLock); + + if (events && events->fileAcquired) + events->fileAcquired(opaque, dataset, acquisition, error); + LG_UNLOCK(clipboard->eventLock); +} + +static void networkClipboardDispatchFileRelease( + NetworkClipboard * clipboard, uint64_t dataset, uint64_t acquisition) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->stateLock); + const LG_ClipboardEventOps * events = clipboard->attached ? + clipboard->events : NULL; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->stateLock); + + if (events && events->fileRelease) + events->fileRelease(opaque, dataset, acquisition); + LG_UNLOCK(clipboard->eventLock); +} + +static void networkClipboardDispatchFileRequest( + NetworkClipboard * clipboard, + const LG_ClipboardFileRequest * request) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->stateLock); + const LG_ClipboardEventOps * events = clipboard->attached ? + clipboard->events : NULL; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->stateLock); + + if (events && events->fileRequest) + events->fileRequest(opaque, request); + LG_UNLOCK(clipboard->eventLock); +} + +static LG_ClipboardResult networkClipboardDispatchFileDataBegin( + NetworkClipboard * clipboard, + const LG_ClipboardFileRequest * request, uint64_t sizeHint) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->stateLock); + const LG_ClipboardEventOps * events = clipboard->attached ? + clipboard->events : NULL; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->stateLock); + + const LG_ClipboardResult result = events && events->fileDataBegin ? + events->fileDataBegin(opaque, request, sizeHint) : + LG_CLIPBOARD_RESULT_FAILED; + LG_UNLOCK(clipboard->eventLock); + return result; +} + +static LG_ClipboardResult networkClipboardDispatchFileDataChunk( + NetworkClipboard * clipboard, + const LG_ClipboardFileRequest * request, uint64_t offset, + const void * data, size_t size) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->stateLock); + const LG_ClipboardEventOps * events = clipboard->attached ? + clipboard->events : NULL; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->stateLock); + + const LG_ClipboardResult result = events && events->fileDataChunk ? + events->fileDataChunk(opaque, request, offset, data, size) : + LG_CLIPBOARD_RESULT_FAILED; + LG_UNLOCK(clipboard->eventLock); + return result; +} + +static LG_ClipboardResult networkClipboardDispatchFileDataEnd( + NetworkClipboard * clipboard, + const LG_ClipboardFileRequest * request, uint64_t finalSize) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->stateLock); + const LG_ClipboardEventOps * events = clipboard->attached ? + clipboard->events : NULL; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->stateLock); + + const LG_ClipboardResult result = events && events->fileDataEnd ? + events->fileDataEnd(opaque, request, finalSize) : + LG_CLIPBOARD_RESULT_FAILED; + LG_UNLOCK(clipboard->eventLock); + return result; +} + +static void networkClipboardDispatchFileReady( + NetworkClipboard * clipboard, uint64_t request) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->stateLock); + const LG_ClipboardEventOps * events = clipboard->attached ? + clipboard->events : NULL; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->stateLock); + + if (events && events->fileDataReady) + events->fileDataReady(opaque, request); + LG_UNLOCK(clipboard->eventLock); +} + +static void networkClipboardDispatchFileCancel( + NetworkClipboard * clipboard, uint64_t dataset, uint64_t request, + LG_ClipboardFileError reason) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->stateLock); + const LG_ClipboardEventOps * events = clipboard->attached ? + clipboard->events : NULL; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->stateLock); + + if (events && events->fileCancel) + events->fileCancel(opaque, dataset, request, reason); + LG_UNLOCK(clipboard->eventLock); +} + +static bool networkClipboardPublishRemoteOffer( + NetworkClipboard * clipboard) +{ + LG_ClipboardData types[NETWORK_CLIPBOARD_FORMATS]; + uint64_t dataset; + uint32_t count; + bool files; + + LG_LOCK(clipboard->stateLock); + struct NetworkRemoteOffer * offer = &clipboard->remoteOffer; + if (!offer->complete || offer->published || + ((offer->formatMask & + (UINT32_C(1) << LG_CLIPBOARD_DATA_FILES)) && !offer->fileOffer)) + { + LG_UNLOCK(clipboard->stateLock); + return false; + } + + count = offer->formatCount; + dataset = offer->offerID; + files = (offer->formatMask & + (UINT32_C(1) << LG_CLIPBOARD_DATA_FILES)) != 0; + memcpy(types, offer->types, count * sizeof(*types)); + offer->published = true; + LG_UNLOCK(clipboard->stateLock); + + networkClipboardDispatchNotice( + clipboard, dataset, types, count, files); + return true; +} + +static bool networkClipboardQueueSendNL(NetworkClipboard * clipboard, + struct NetworkClipboardSend * send) +{ + if (!send) + return false; + + struct NetworkClipboardBatch batch = { 0 }; + networkClipboardBatchAppend(&batch, send); + if (networkClipboardQueueBatchNL(clipboard, &batch)) + return true; + + networkClipboardBatchFree(&batch); + return false; +} + +static void networkClipboardRemoteOfferResetNL( + NetworkClipboard * clipboard) +{ + memset(&clipboard->remoteOffer, 0, sizeof(clipboard->remoteOffer)); + for (size_t index = 0; index < LG_CLIPBOARD_DATA_NONE; ++index) + clipboard->remoteOffer.indices[index] = UINT32_MAX; +} + +static bool networkClipboardEnvelopeValid( + const struct NetworkClipboardInbound * packet, LGNetService service) +{ + const bool reliable = + packet->delivery == NETWORK_PACKET_DELIVERY_RELIABLE; + const bool bulk = + packet->delivery == NETWORK_PACKET_DELIVERY_BULK; + const LGNetEnvelopeFlags required = LG_NET_ENVELOPE_FINAL | + (reliable ? LG_NET_ENVELOPE_RELIABLE : 0); + const LGNetEnvelopeFlags prohibited = LG_NET_ENVELOPE_DATAGRAM | + LG_NET_ENVELOPE_EARLY_DATA | + (bulk ? LG_NET_ENVELOPE_RELIABLE : 0); + return packet->envelope.service == service && + (reliable || bulk) && + (packet->envelope.flags & required) == required && + !(packet->envelope.flags & prohibited); +} + +static bool networkClipboardEpochCurrentNL( + const NetworkClipboard * clipboard, + const struct NetworkClipboardInbound * packet) +{ + return (clipboard->claimPending || clipboard->claimed) && + packet->envelope.componentEpoch == clipboard->claimEpoch; +} + +static bool networkClipboardQueueClipboardStatusNL( + NetworkClipboard * clipboard, uint64_t wireRequest, uint64_t offerID, + LGNetClipboardStatusCode code, uint32_t detail, uint64_t processed) +{ + const LGNetClipboardStatus status = + { + .requestID = wireRequest, + .offerID = offerID, + .status = code, + .detail = detail, + .processedLength = processed, + }; + return networkClipboardQueueSendNL(clipboard, + networkClipboardStatusSendNew(clipboard, &status, + LG_NET_ENVELOPE_RESPONSE)); +} + +static bool networkClipboardQueueFileStatusNL( + NetworkClipboard * clipboard, uint64_t wireRequest, uint64_t offerID, + uint64_t entryID, LG_ClipboardFileError error, uint64_t transferred) +{ + const LGNetFileStatus status = + { + .requestID = wireRequest, + .offerID = offerID, + .entryID = entryID, + .status = networkClipboardFileStatus(error), + .detail = networkClipboardFileErrorToKVMFR(error), + .transferredLength = transferred, + }; + return networkClipboardQueueSendNL(clipboard, + networkClipboardFileStatusSendNew(clipboard, &status, + LG_NET_ENVELOPE_RESPONSE)); +} + +enum NetworkClipboardProcessResult +{ + NETWORK_CLIPBOARD_PROCESS_INVALID, + NETWORK_CLIPBOARD_PROCESS_CONSUMED, + NETWORK_CLIPBOARD_PROCESS_BLOCKED, +}; + +static enum NetworkClipboardProcessResult +networkClipboardProcessClaimResult(NetworkClipboard * clipboard, + const struct NetworkClipboardInbound * packet) +{ + LGNetClipboardClaimStatus status; + if (!(packet->envelope.flags & LG_NET_ENVELOPE_RESPONSE) || + lgNetClipboardClaimStatusDecode(&status, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + status.claimEpoch != packet->envelope.requestID) + return NETWORK_CLIPBOARD_PROCESS_INVALID; + + LG_LOCK(clipboard->stateLock); + if (!networkClipboardEpochCurrentNL(clipboard, packet) || + status.claimEpoch != clipboard->claimEpoch) + { + LG_UNLOCK(clipboard->stateLock); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; + } + if (clipboard->lastClaimStatusSerial && + status.serial <= clipboard->lastClaimStatusSerial) + { + LG_UNLOCK(clipboard->stateLock); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; + } + + clipboard->lastClaimStatusSerial = status.serial; + clipboard->claimPending = false; + if (status.status == LG_NET_CLIPBOARD_CLAIM_ACCEPTED && + (status.flags & NETWORK_CLIPBOARD_CLAIM_FLAGS) == + NETWORK_CLIPBOARD_CLAIM_FLAGS) + { + clipboard->claimed = true; + clipboard->nextClaimNs = 0; + clipboard->nextKeepaliveNs = networkClipboardDeadline( + NETWORK_CLIPBOARD_KEEPALIVE_MS); + if (clipboard->localOffer.offerID) + networkClipboardQueueLocalOfferNL(clipboard); + } + else + { + clipboard->claimed = false; + clipboard->nextClaimNs = networkClipboardDeadline( + NETWORK_CLIPBOARD_CLAIM_RETRY_MS); + clipboard->nextKeepaliveNs = 0; + } + LG_UNLOCK(clipboard->stateLock); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; +} + +static enum NetworkClipboardProcessResult networkClipboardProcessOffer( + NetworkClipboard * clipboard, + const struct NetworkClipboardInbound * packet) +{ + LGNetClipboardOffer offer; + if (packet->envelope.flags & LG_NET_ENVELOPE_RESPONSE || + lgNetClipboardOfferDecode(&offer, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + offer.formatCount > NETWORK_CLIPBOARD_FORMATS || + packet->envelope.requestID != offer.offerID) + return NETWORK_CLIPBOARD_PROCESS_INVALID; + + const KVMFRClipboardFormat format = + lgNetClipboardKVMFRFormatFromMime(offer.mime, offer.mimeLength); + const LG_ClipboardData type = networkClipboardDataFromFormat(format); + if (!networkClipboardDataValid(type)) + return NETWORK_CLIPBOARD_PROCESS_INVALID; + + LG_LOCK(clipboard->stateLock); + if (!networkClipboardEpochCurrentNL(clipboard, packet)) + { + LG_UNLOCK(clipboard->stateLock); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; + } + + struct NetworkRemoteOffer * remote = &clipboard->remoteOffer; + if (!remote->formatCount || remote->epoch != packet->envelope.componentEpoch || + remote->offerID != offer.offerID || remote->serial != offer.serial) + { + const bool fileOffer = remote->fileOffer && + remote->epoch == packet->envelope.componentEpoch && + remote->offerID == offer.offerID; + networkClipboardRemoteOfferResetNL(clipboard); + remote->epoch = packet->envelope.componentEpoch; + remote->offerID = offer.offerID; + remote->serial = offer.serial; + remote->formatCount = offer.formatCount; + remote->fileOffer = fileOffer; + } + else if (remote->formatCount != offer.formatCount) + { + LG_UNLOCK(clipboard->stateLock); + return NETWORK_CLIPBOARD_PROCESS_INVALID; + } + + const uint32_t indexBit = UINT32_C(1) << offer.formatIndex; + const uint32_t typeBit = UINT32_C(1) << type; + if ((remote->received & indexBit) || (remote->formatMask & typeBit)) + { + LG_UNLOCK(clipboard->stateLock); + return NETWORK_CLIPBOARD_PROCESS_INVALID; + } + + remote->types[offer.formatIndex] = type; + remote->indices[type] = offer.formatIndex; + remote->received |= indexBit; + remote->formatMask |= typeBit; + const uint32_t completeMask = + (UINT32_C(1) << remote->formatCount) - 1U; + remote->complete = remote->received == completeMask; + LG_UNLOCK(clipboard->stateLock); + + networkClipboardPublishRemoteOffer(clipboard); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; +} + +static enum NetworkClipboardProcessResult networkClipboardProcessClear( + NetworkClipboard * clipboard, + const struct NetworkClipboardInbound * packet) +{ + LGNetClipboardControl control; + if (packet->envelope.flags & LG_NET_ENVELOPE_RESPONSE || + lgNetClipboardControlDecode(&control, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK) + return NETWORK_CLIPBOARD_PROCESS_INVALID; + + bool released = false; + LG_LOCK(clipboard->stateLock); + if (!networkClipboardEpochCurrentNL(clipboard, packet)) + { + LG_UNLOCK(clipboard->stateLock); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; + } + if (clipboard->remoteOffer.offerID && + (!control.offerID || + control.offerID == clipboard->remoteOffer.offerID)) + { + released = clipboard->remoteOffer.published; + networkClipboardRemoteOfferResetNL(clipboard); + } + LG_UNLOCK(clipboard->stateLock); + + if (released) + networkClipboardDispatchRelease(clipboard); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; +} + +static enum NetworkClipboardProcessResult networkClipboardProcessRequest( + NetworkClipboard * clipboard, + const struct NetworkClipboardInbound * packet) +{ + LGNetClipboardRequest request; + if (packet->envelope.flags & LG_NET_ENVELOPE_RESPONSE || + !(packet->envelope.flags & LG_NET_ENVELOPE_ACK_REQUIRED) || + lgNetClipboardRequestDecode(&request, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + request.requestID != packet->envelope.requestID || request.offset) + return NETWORK_CLIPBOARD_PROCESS_INVALID; + + struct NetworkClipboardTransfer * transfer = calloc(1, sizeof(*transfer)); + if (!transfer) + return NETWORK_CLIPBOARD_PROCESS_INVALID; + + LG_LOCK(clipboard->stateLock); + if (!networkClipboardEpochCurrentNL(clipboard, packet) || + request.offerID != clipboard->localOffer.offerID || + request.formatIndex >= clipboard->localOffer.count || + networkClipboardTransferCount(clipboard->clipboardWrites) >= + NETWORK_CLIPBOARD_FILE_REQUESTS || + networkClipboardTransferFindWire( + clipboard->clipboardWrites, request.requestID)) + { + LG_UNLOCK(clipboard->stateLock); + free(transfer); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; + } + + transfer->localRequest = networkClipboardNextLocalIDNL(clipboard, true); + transfer->wireRequest = request.requestID; + transfer->offerID = request.offerID; + transfer->type = clipboard->localOffer.types[request.formatIndex]; + transfer->formatIndex = request.formatIndex; + transfer->maxChunkLength = request.maxChunkLength < + NETWORK_CLIPBOARD_EMITTED_CHUNK ? request.maxChunkLength : + NETWORK_CLIPBOARD_EMITTED_CHUNK; + transfer->next = clipboard->clipboardWrites; + clipboard->clipboardWrites = transfer; + + const LG_ClipboardRequest localRequest = transfer->localRequest; + const LG_ClipboardData type = transfer->type; + LG_UNLOCK(clipboard->stateLock); + + if (networkClipboardDispatchRequest(clipboard, localRequest, type)) + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; + + LG_LOCK(clipboard->stateLock); + transfer = networkClipboardTransferTake( + &clipboard->clipboardWrites, localRequest); + if (transfer) + networkClipboardQueueClipboardStatusNL(clipboard, + transfer->wireRequest, transfer->offerID, + LG_NET_CLIPBOARD_STATUS_UNSUPPORTED, 0, 0); + LG_UNLOCK(clipboard->stateLock); + free(transfer); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; +} + +static void networkClipboardRejectRead(NetworkClipboard * clipboard, + uint64_t wireRequest, LG_ClipboardCancelReason reason) +{ + LG_ClipboardRequest localRequest = LG_CLIPBOARD_REQUEST_INVALID; + + LG_LOCK(clipboard->stateLock); + struct NetworkClipboardTransfer * transfer = + networkClipboardTransferTakeWire( + &clipboard->clipboardReads, wireRequest); + if (transfer) + { + localRequest = transfer->localRequest; + networkClipboardQueueClipboardStatusNL(clipboard, + transfer->wireRequest, transfer->offerID, + LG_NET_CLIPBOARD_STATUS_CANCELLED, (uint32_t)reason, + transfer->offset); + } + LG_UNLOCK(clipboard->stateLock); + + free(transfer); + if (localRequest) + networkClipboardDispatchDataCancel( + clipboard, localRequest, reason); +} + +static enum NetworkClipboardProcessResult +networkClipboardProcessDataBegin(NetworkClipboard * clipboard, + const struct NetworkClipboardInbound * packet) +{ + LGNetClipboardTransfer wire; + if (packet->envelope.flags & LG_NET_ENVELOPE_RESPONSE || + lgNetClipboardTransferDecode(&wire, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + wire.requestID != packet->envelope.requestID || wire.processedLength) + return NETWORK_CLIPBOARD_PROCESS_INVALID; + + LG_ClipboardRequest localRequest; + LG_ClipboardData type; + LG_LOCK(clipboard->stateLock); + if (!networkClipboardEpochCurrentNL(clipboard, packet)) + { + LG_UNLOCK(clipboard->stateLock); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; + } + + struct NetworkClipboardTransfer * transfer = + networkClipboardTransferFindWire( + clipboard->clipboardReads, wire.requestID); + if (!transfer || transfer->offerID != wire.offerID || transfer->began) + { + LG_UNLOCK(clipboard->stateLock); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; + } + localRequest = transfer->localRequest; + type = transfer->type; + LG_UNLOCK(clipboard->stateLock); + + const LG_ClipboardResult result = networkClipboardDispatchDataBegin( + clipboard, localRequest, type, wire.totalLength); + if (result == LG_CLIPBOARD_RESULT_BLOCKED) + { + LG_LOCK(clipboard->stateLock); + clipboard->inboundBlockedRequest = localRequest; + LG_UNLOCK(clipboard->stateLock); + return NETWORK_CLIPBOARD_PROCESS_BLOCKED; + } + if (result != LG_CLIPBOARD_RESULT_ACCEPTED) + { + networkClipboardRejectRead( + clipboard, wire.requestID, LG_CLIPBOARD_CANCEL_INVALID); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; + } + + LG_LOCK(clipboard->stateLock); + transfer = networkClipboardTransferFindWire( + clipboard->clipboardReads, wire.requestID); + if (transfer && !transfer->began) + { + transfer->began = true; + transfer->sizeHint = wire.totalLength; + transfer->offset = 0; + transfer->delivery = packet->delivery; + } + LG_UNLOCK(clipboard->stateLock); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; +} + +static enum NetworkClipboardProcessResult +networkClipboardProcessDataChunk(NetworkClipboard * clipboard, + const struct NetworkClipboardInbound * packet) +{ + LGNetClipboardChunk chunk; + if (packet->envelope.flags & LG_NET_ENVELOPE_RESPONSE || + lgNetClipboardChunkDecode(&chunk, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + chunk.requestID != packet->envelope.requestID || + chunk.dataLength > NETWORK_CLIPBOARD_EMITTED_CHUNK) + return NETWORK_CLIPBOARD_PROCESS_INVALID; + + LG_ClipboardRequest localRequest; + LG_LOCK(clipboard->stateLock); + if (!networkClipboardEpochCurrentNL(clipboard, packet)) + { + LG_UNLOCK(clipboard->stateLock); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; + } + + struct NetworkClipboardTransfer * transfer = + networkClipboardTransferFindWire( + clipboard->clipboardReads, chunk.requestID); + if (!transfer || !transfer->began || transfer->offerID != chunk.offerID || + transfer->delivery != packet->delivery || + transfer->offset != chunk.offset || + (transfer->sizeHint != LG_CLIPBOARD_SIZE_UNKNOWN && + transfer->sizeHint != chunk.totalLength)) + { + LG_UNLOCK(clipboard->stateLock); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; + } + localRequest = transfer->localRequest; + LG_UNLOCK(clipboard->stateLock); + + const LG_ClipboardResult result = networkClipboardDispatchDataChunk( + clipboard, localRequest, chunk.offset, chunk.data, chunk.dataLength); + if (result == LG_CLIPBOARD_RESULT_BLOCKED) + { + LG_LOCK(clipboard->stateLock); + clipboard->inboundBlockedRequest = localRequest; + LG_UNLOCK(clipboard->stateLock); + return NETWORK_CLIPBOARD_PROCESS_BLOCKED; + } + if (result != LG_CLIPBOARD_RESULT_ACCEPTED) + { + networkClipboardRejectRead( + clipboard, chunk.requestID, LG_CLIPBOARD_CANCEL_INVALID); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; + } + + LG_LOCK(clipboard->stateLock); + transfer = networkClipboardTransferFindWire( + clipboard->clipboardReads, chunk.requestID); + if (transfer && transfer->offset == chunk.offset) + transfer->offset += chunk.dataLength; + LG_UNLOCK(clipboard->stateLock); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; +} + +static enum NetworkClipboardProcessResult networkClipboardProcessDataEnd( + NetworkClipboard * clipboard, + const struct NetworkClipboardInbound * packet) +{ + LGNetClipboardTransfer wire; + if (packet->envelope.flags & LG_NET_ENVELOPE_RESPONSE || + lgNetClipboardTransferDecode(&wire, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + wire.requestID != packet->envelope.requestID || + wire.processedLength != wire.totalLength) + return NETWORK_CLIPBOARD_PROCESS_INVALID; + + LG_ClipboardRequest localRequest; + LG_LOCK(clipboard->stateLock); + if (!networkClipboardEpochCurrentNL(clipboard, packet)) + { + LG_UNLOCK(clipboard->stateLock); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; + } + + struct NetworkClipboardTransfer * transfer = + networkClipboardTransferFindWire( + clipboard->clipboardReads, wire.requestID); + if (!transfer || !transfer->began || transfer->offerID != wire.offerID || + transfer->delivery != packet->delivery || + transfer->offset != wire.processedLength || + (transfer->sizeHint != LG_CLIPBOARD_SIZE_UNKNOWN && + transfer->sizeHint != wire.totalLength)) + { + LG_UNLOCK(clipboard->stateLock); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; + } + localRequest = transfer->localRequest; + LG_UNLOCK(clipboard->stateLock); + + const LG_ClipboardResult result = networkClipboardDispatchDataEnd( + clipboard, localRequest, wire.totalLength); + if (result == LG_CLIPBOARD_RESULT_BLOCKED) + { + LG_LOCK(clipboard->stateLock); + clipboard->inboundBlockedRequest = localRequest; + LG_UNLOCK(clipboard->stateLock); + return NETWORK_CLIPBOARD_PROCESS_BLOCKED; + } + if (result != LG_CLIPBOARD_RESULT_ACCEPTED) + { + networkClipboardRejectRead( + clipboard, wire.requestID, LG_CLIPBOARD_CANCEL_INVALID); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; + } + + LG_LOCK(clipboard->stateLock); + transfer = networkClipboardTransferTakeWire( + &clipboard->clipboardReads, wire.requestID); + LG_UNLOCK(clipboard->stateLock); + free(transfer); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; +} + +static enum NetworkClipboardProcessResult +networkClipboardProcessDataReadyMessage(NetworkClipboard * clipboard, + const struct NetworkClipboardInbound * packet) +{ + LGNetClipboardTransfer wire; + if (packet->envelope.flags & LG_NET_ENVELOPE_RESPONSE || + lgNetClipboardTransferDecode(&wire, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + wire.requestID != packet->envelope.requestID || wire.processedLength) + return NETWORK_CLIPBOARD_PROCESS_INVALID; + + LG_ClipboardRequest localRequest = LG_CLIPBOARD_REQUEST_INVALID; + LG_LOCK(clipboard->stateLock); + if (networkClipboardEpochCurrentNL(clipboard, packet)) + { + struct NetworkClipboardTransfer * transfer = + networkClipboardTransferFindWire( + clipboard->clipboardWrites, wire.requestID); + if (transfer && transfer->offerID == wire.offerID && + transfer->blocked) + { + transfer->blocked = false; + localRequest = transfer->localRequest; + } + } + LG_UNLOCK(clipboard->stateLock); + + if (localRequest) + networkClipboardDispatchDataReady(clipboard, localRequest); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; +} + +static enum NetworkClipboardProcessResult networkClipboardProcessCancel( + NetworkClipboard * clipboard, + const struct NetworkClipboardInbound * packet) +{ + LGNetClipboardTransfer wire; + if (lgNetClipboardTransferDecode(&wire, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + wire.requestID != packet->envelope.requestID) + return NETWORK_CLIPBOARD_PROCESS_INVALID; + + LG_ClipboardRequest localRead = LG_CLIPBOARD_REQUEST_INVALID; + LG_ClipboardRequest localWrite = LG_CLIPBOARD_REQUEST_INVALID; + LG_LOCK(clipboard->stateLock); + if (networkClipboardEpochCurrentNL(clipboard, packet)) + { + struct NetworkClipboardTransfer * read = + networkClipboardTransferFindWire( + clipboard->clipboardReads, wire.requestID); + struct NetworkClipboardTransfer * write = + networkClipboardTransferFindWire( + clipboard->clipboardWrites, wire.requestID); + if (read && read->offerID == wire.offerID) + { + read = networkClipboardTransferTakeWire( + &clipboard->clipboardReads, wire.requestID); + localRead = read->localRequest; + } + else + read = NULL; + if (write && write->offerID == wire.offerID) + { + write = networkClipboardTransferTakeWire( + &clipboard->clipboardWrites, wire.requestID); + localWrite = write->localRequest; + } + else + write = NULL; + free(read); + free(write); + } + LG_UNLOCK(clipboard->stateLock); + + if (localRead) + networkClipboardDispatchDataCancel( + clipboard, localRead, LG_CLIPBOARD_CANCEL_ABORTED); + if (localWrite) + networkClipboardDispatchRequestCancel( + clipboard, localWrite, LG_CLIPBOARD_CANCEL_ABORTED); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; +} + +static enum NetworkClipboardProcessResult networkClipboardProcessStatus( + NetworkClipboard * clipboard, + const struct NetworkClipboardInbound * packet) +{ + LGNetClipboardStatus status; + if (!(packet->envelope.flags & LG_NET_ENVELOPE_RESPONSE) || + lgNetClipboardStatusDecode(&status, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + status.requestID != packet->envelope.requestID) + return NETWORK_CLIPBOARD_PROCESS_INVALID; + + if (status.status == LG_NET_CLIPBOARD_STATUS_OK) + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; + + const LG_ClipboardCancelReason reason = + status.status == LG_NET_CLIPBOARD_STATUS_CANCELLED && + status.detail <= LG_CLIPBOARD_CANCEL_INVALID ? + (LG_ClipboardCancelReason)status.detail : + LG_CLIPBOARD_CANCEL_UNAVAILABLE; + LG_ClipboardRequest localRead = LG_CLIPBOARD_REQUEST_INVALID; + LG_ClipboardRequest localWrite = LG_CLIPBOARD_REQUEST_INVALID; + LG_LOCK(clipboard->stateLock); + if (networkClipboardEpochCurrentNL(clipboard, packet)) + { + struct NetworkClipboardTransfer * read = + networkClipboardTransferFindWire( + clipboard->clipboardReads, status.requestID); + struct NetworkClipboardTransfer * write = + networkClipboardTransferFindWire( + clipboard->clipboardWrites, status.requestID); + if (read && read->offerID == status.offerID) + { + read = networkClipboardTransferTakeWire( + &clipboard->clipboardReads, status.requestID); + localRead = read->localRequest; + } + else + read = NULL; + if (write && write->offerID == status.offerID) + { + write = networkClipboardTransferTakeWire( + &clipboard->clipboardWrites, status.requestID); + localWrite = write->localRequest; + } + else + write = NULL; + free(read); + free(write); + } + LG_UNLOCK(clipboard->stateLock); + + if (localRead) + networkClipboardDispatchDataCancel( + clipboard, localRead, reason); + if (localWrite) + networkClipboardDispatchRequestCancel( + clipboard, localWrite, reason); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; +} + +static enum NetworkClipboardProcessResult networkClipboardProcessFileOffer( + NetworkClipboard * clipboard, + const struct NetworkClipboardInbound * packet) +{ + LGNetFileOffer offer; + if (packet->envelope.flags & LG_NET_ENVELOPE_RESPONSE || + lgNetFileOfferDecode(&offer, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + packet->envelope.requestID != offer.offerID || offer.totalBytes || + offer.entryCount != 1 || offer.labelLength || + offer.flags != (LG_NET_FILE_OFFER_COPY | + LG_NET_FILE_OFFER_RECURSIVE)) + return NETWORK_CLIPBOARD_PROCESS_INVALID; + + LG_LOCK(clipboard->stateLock); + if (!networkClipboardEpochCurrentNL(clipboard, packet)) + { + LG_UNLOCK(clipboard->stateLock); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; + } + + struct NetworkRemoteOffer * remote = &clipboard->remoteOffer; + if (remote->offerID && + (remote->epoch != packet->envelope.componentEpoch || + remote->offerID != offer.offerID)) + networkClipboardRemoteOfferResetNL(clipboard); + remote->epoch = packet->envelope.componentEpoch; + remote->offerID = offer.offerID; + remote->fileOffer = true; + LG_UNLOCK(clipboard->stateLock); + + networkClipboardPublishRemoteOffer(clipboard); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; +} + +static enum NetworkClipboardProcessResult +networkClipboardProcessFileAcquire(NetworkClipboard * clipboard, + const struct NetworkClipboardInbound * packet) +{ + LGNetFileLease lease; + if (packet->envelope.flags & LG_NET_ENVELOPE_RESPONSE || + !(packet->envelope.flags & LG_NET_ENVELOPE_ACK_REQUIRED) || + lgNetFileLeaseDecode(&lease, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + lease.clientID != packet->envelope.requestID || lease.leaseEpoch || + lease.flags != LG_NET_FILE_LEASE_READ) + return NETWORK_CLIPBOARD_PROCESS_INVALID; + + struct NetworkFileAcquisition * acquisition = + calloc(1, sizeof(*acquisition)); + if (!acquisition) + return NETWORK_CLIPBOARD_PROCESS_INVALID; + + LG_LOCK(clipboard->stateLock); + if (!networkClipboardEpochCurrentNL(clipboard, packet) || + lease.offerID != clipboard->localOffer.offerID || + !clipboard->localOffer.files || + networkClipboardAcquisitionCountNL(clipboard) >= + NETWORK_CLIPBOARD_FILE_ACQUISITIONS || + networkClipboardAcquisitionFindWire( + clipboard, lease.offerID, lease.clientID, true)) + { + LG_UNLOCK(clipboard->stateLock); + free(acquisition); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; + } + + acquisition->dataset = lease.offerID; + acquisition->localAcquisition = + networkClipboardNextLocalIDNL(clipboard, true); + acquisition->wireClientID = lease.clientID; + acquisition->fromRemote = true; + + acquisition->next = clipboard->fileAcquisitions; + clipboard->fileAcquisitions = acquisition; + + const uint64_t localAcquisition = acquisition->localAcquisition; + LG_UNLOCK(clipboard->stateLock); + + networkClipboardDispatchFileAcquire( + clipboard, lease.offerID, localAcquisition); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; +} + +static enum NetworkClipboardProcessResult +networkClipboardProcessFileAcquired(NetworkClipboard * clipboard, + const struct NetworkClipboardInbound * packet) +{ + LGNetFileLease lease; + if (!(packet->envelope.flags & LG_NET_ENVELOPE_RESPONSE) || + lgNetFileLeaseDecode(&lease, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + lease.clientID != packet->envelope.requestID || !lease.leaseEpoch || + lease.flags != (LG_NET_FILE_LEASE_READ | + LG_NET_FILE_LEASE_ACQUIRED)) + return NETWORK_CLIPBOARD_PROCESS_INVALID; + + uint64_t localAcquisition = 0; + LG_LOCK(clipboard->stateLock); + if (networkClipboardEpochCurrentNL(clipboard, packet)) + { + struct NetworkFileAcquisition * acquisition = + networkClipboardAcquisitionFindWire( + clipboard, lease.offerID, lease.clientID, false); + if (acquisition && !acquisition->active) + { + acquisition->leaseEpoch = lease.leaseEpoch; + acquisition->active = true; + localAcquisition = acquisition->localAcquisition; + } + } + LG_UNLOCK(clipboard->stateLock); + + if (localAcquisition) + networkClipboardDispatchFileAcquired(clipboard, lease.offerID, + localAcquisition, LG_CLIPBOARD_FILE_ERROR_NONE); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; +} + +static enum NetworkClipboardProcessResult +networkClipboardProcessFileReleaseMessage(NetworkClipboard * clipboard, + const struct NetworkClipboardInbound * packet) +{ + LGNetFileLease lease; + if (packet->envelope.flags & LG_NET_ENVELOPE_RESPONSE || + lgNetFileLeaseDecode(&lease, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + lease.clientID != packet->envelope.requestID || !lease.leaseEpoch || + lease.leaseMs || lease.flags != LG_NET_FILE_LEASE_READ) + return NETWORK_CLIPBOARD_PROCESS_INVALID; + + uint64_t localAcquisition = 0; + LG_LOCK(clipboard->stateLock); + if (networkClipboardEpochCurrentNL(clipboard, packet)) + { + struct NetworkFileAcquisition * acquisition = + networkClipboardAcquisitionFindWire( + clipboard, lease.offerID, lease.clientID, true); + if (acquisition && acquisition->active && + acquisition->leaseEpoch == lease.leaseEpoch) + { + localAcquisition = acquisition->localAcquisition; + acquisition = networkClipboardAcquisitionTake(clipboard, + lease.offerID, localAcquisition, true); + free(acquisition); + } + } + LG_UNLOCK(clipboard->stateLock); + + if (localAcquisition) + networkClipboardDispatchFileRelease( + clipboard, lease.offerID, localAcquisition); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; +} + +static enum NetworkClipboardProcessResult +networkClipboardProcessFileRequestMessage(NetworkClipboard * clipboard, + const struct NetworkClipboardInbound * packet) +{ + LGNetFileRequest wire; + if (packet->envelope.flags & LG_NET_ENVELOPE_RESPONSE || + !(packet->envelope.flags & LG_NET_ENVELOPE_ACK_REQUIRED) || + lgNetFileRequestDecode(&wire, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + wire.requestID != packet->envelope.requestID) + return NETWORK_CLIPBOARD_PROCESS_INVALID; + + const KVMFRClipboardFileOperation operation = + lgNetFileKVMFROperationFromRequestFlags(wire.flags); + uint64_t node; + if (!operation || + !lgNetFileKVMFRNodeFromEntryID(wire.entryID, &node) || + (operation == KVMFR_CLIPBOARD_FILE_OP_READ && + wire.length > NETWORK_CLIPBOARD_FILE_READ_MAX)) + return NETWORK_CLIPBOARD_PROCESS_INVALID; + + struct NetworkFileTransfer * transfer = calloc(1, sizeof(*transfer)); + if (!transfer) + return NETWORK_CLIPBOARD_PROCESS_INVALID; + + LG_LOCK(clipboard->stateLock); + if (!networkClipboardEpochCurrentNL(clipboard, packet) || + wire.offerID != clipboard->localOffer.offerID || + !clipboard->localOffer.files || + !networkClipboardDatasetAcquiredNL( + clipboard, wire.offerID, true) || + networkClipboardFileTransferCountNL(clipboard) >= + NETWORK_CLIPBOARD_FILE_REQUESTS || + networkClipboardFileTransferFindWire( + clipboard->fileWrites, wire.requestID)) + { + LG_UNLOCK(clipboard->stateLock); + free(transfer); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; + } + + transfer->request = (LG_ClipboardFileRequest) + { + .dataset = wire.offerID, + .request = networkClipboardNextLocalIDNL(clipboard, true), + .node = node, + .offset = wire.offset, + .length = wire.length, + .operation = operation == KVMFR_CLIPBOARD_FILE_OP_READ ? + LG_CLIPBOARD_FILE_READ : LG_CLIPBOARD_FILE_LIST, + }; + transfer->wireRequest = wire.requestID; + transfer->entryID = wire.entryID; + transfer->next = clipboard->fileWrites; + clipboard->fileWrites = transfer; + + const LG_ClipboardFileRequest request = transfer->request; + LG_UNLOCK(clipboard->stateLock); + + networkClipboardDispatchFileRequest(clipboard, &request); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; +} + +static void networkClipboardRejectFileRead(NetworkClipboard * clipboard, + uint64_t wireRequest, LG_ClipboardFileError reason) +{ + LG_ClipboardFileRequest request = { 0 }; + + LG_LOCK(clipboard->stateLock); + struct NetworkFileTransfer * transfer = + networkClipboardFileTransferTakeWire( + &clipboard->fileReads, wireRequest); + if (transfer) + { + request = transfer->request; + networkClipboardQueueFileStatusNL(clipboard, + transfer->wireRequest, transfer->request.dataset, + transfer->entryID, reason, transfer->responseOffset); + } + LG_UNLOCK(clipboard->stateLock); + + free(transfer); + if (request.request) + networkClipboardDispatchFileCancel( + clipboard, request.dataset, request.request, reason); +} + +static enum NetworkClipboardProcessResult +networkClipboardProcessFileDataBegin(NetworkClipboard * clipboard, + const struct NetworkClipboardInbound * packet) +{ + LGNetFileTransfer wire; + if (packet->envelope.flags & LG_NET_ENVELOPE_RESPONSE || + lgNetFileTransferDecode(&wire, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + wire.requestID != packet->envelope.requestID || wire.processedLength) + return NETWORK_CLIPBOARD_PROCESS_INVALID; + + LG_ClipboardFileRequest request; + LG_LOCK(clipboard->stateLock); + if (!networkClipboardEpochCurrentNL(clipboard, packet)) + { + LG_UNLOCK(clipboard->stateLock); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; + } + + struct NetworkFileTransfer * transfer = + networkClipboardFileTransferFindWire( + clipboard->fileReads, wire.requestID); + if (!transfer || transfer->began || + transfer->request.dataset != wire.offerID || + transfer->entryID != wire.entryID || + transfer->request.offset != wire.offset || + (transfer->request.operation == LG_CLIPBOARD_FILE_READ && + (wire.totalLength - wire.offset > NETWORK_CLIPBOARD_FILE_READ_MAX || + wire.totalLength - wire.offset > transfer->request.length))) + { + LG_UNLOCK(clipboard->stateLock); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; + } + request = transfer->request; + LG_UNLOCK(clipboard->stateLock); + + const LG_ClipboardResult result = networkClipboardDispatchFileDataBegin( + clipboard, &request, wire.totalLength - wire.offset); + if (result == LG_CLIPBOARD_RESULT_BLOCKED) + { + networkClipboardRejectFileRead(clipboard, wire.requestID, + LG_CLIPBOARD_FILE_ERROR_IO); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; + } + if (result != LG_CLIPBOARD_RESULT_ACCEPTED) + { + networkClipboardRejectFileRead(clipboard, wire.requestID, + LG_CLIPBOARD_FILE_ERROR_INVALID); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; + } + + LG_LOCK(clipboard->stateLock); + transfer = networkClipboardFileTransferFindWire( + clipboard->fileReads, wire.requestID); + if (transfer && !transfer->began) + { + transfer->began = true; + transfer->sizeHint = wire.totalLength - wire.offset; + transfer->responseOffset = 0; + transfer->delivery = packet->delivery; + } + LG_UNLOCK(clipboard->stateLock); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; +} + +static enum NetworkClipboardProcessResult +networkClipboardProcessFileDataChunk(NetworkClipboard * clipboard, + const struct NetworkClipboardInbound * packet) +{ + LGNetFileChunk chunk; + if (packet->envelope.flags & LG_NET_ENVELOPE_RESPONSE || + lgNetFileChunkDecode(&chunk, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + chunk.requestID != packet->envelope.requestID || + chunk.dataLength > NETWORK_CLIPBOARD_EMITTED_CHUNK || + chunk.offset > chunk.totalLength) + return NETWORK_CLIPBOARD_PROCESS_INVALID; + + LG_ClipboardFileRequest request; + uint64_t responseOffset; + LG_LOCK(clipboard->stateLock); + if (!networkClipboardEpochCurrentNL(clipboard, packet)) + { + LG_UNLOCK(clipboard->stateLock); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; + } + + struct NetworkFileTransfer * transfer = + networkClipboardFileTransferFindWire( + clipboard->fileReads, chunk.requestID); + if (!transfer || !transfer->began || + transfer->delivery != packet->delivery || + transfer->request.dataset != chunk.offerID || + transfer->entryID != chunk.entryID || + transfer->responseOffset != chunk.offset || + transfer->sizeHint != chunk.totalLength) + { + LG_UNLOCK(clipboard->stateLock); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; + } + request = transfer->request; + responseOffset = transfer->responseOffset; + LG_UNLOCK(clipboard->stateLock); + + const LG_ClipboardResult result = networkClipboardDispatchFileDataChunk( + clipboard, &request, responseOffset, chunk.data, chunk.dataLength); + if (result != LG_CLIPBOARD_RESULT_ACCEPTED) + { + networkClipboardRejectFileRead(clipboard, chunk.requestID, + result == LG_CLIPBOARD_RESULT_BLOCKED ? + LG_CLIPBOARD_FILE_ERROR_IO : LG_CLIPBOARD_FILE_ERROR_INVALID); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; + } + + LG_LOCK(clipboard->stateLock); + transfer = networkClipboardFileTransferFindWire( + clipboard->fileReads, chunk.requestID); + if (transfer && transfer->responseOffset == responseOffset) + transfer->responseOffset += chunk.dataLength; + LG_UNLOCK(clipboard->stateLock); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; +} + +static enum NetworkClipboardProcessResult +networkClipboardProcessFileDataEnd(NetworkClipboard * clipboard, + const struct NetworkClipboardInbound * packet) +{ + LGNetFileTransfer wire; + if (packet->envelope.flags & LG_NET_ENVELOPE_RESPONSE || + lgNetFileTransferDecode(&wire, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + wire.requestID != packet->envelope.requestID || + wire.processedLength != wire.totalLength - wire.offset) + return NETWORK_CLIPBOARD_PROCESS_INVALID; + + LG_ClipboardFileRequest request; + LG_LOCK(clipboard->stateLock); + if (!networkClipboardEpochCurrentNL(clipboard, packet)) + { + LG_UNLOCK(clipboard->stateLock); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; + } + + struct NetworkFileTransfer * transfer = + networkClipboardFileTransferFindWire( + clipboard->fileReads, wire.requestID); + if (!transfer || !transfer->began || + transfer->delivery != packet->delivery || + transfer->request.dataset != wire.offerID || + transfer->entryID != wire.entryID || + transfer->request.offset != wire.offset || + transfer->responseOffset != wire.processedLength || + transfer->request.offset > UINT64_MAX - transfer->sizeHint || + transfer->request.offset + transfer->sizeHint != wire.totalLength || + (transfer->request.operation == LG_CLIPBOARD_FILE_READ && + transfer->sizeHint > NETWORK_CLIPBOARD_FILE_READ_MAX)) + { + LG_UNLOCK(clipboard->stateLock); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; + } + request = transfer->request; + LG_UNLOCK(clipboard->stateLock); + + const LG_ClipboardResult result = networkClipboardDispatchFileDataEnd( + clipboard, &request, wire.processedLength); + if (result != LG_CLIPBOARD_RESULT_ACCEPTED) + { + networkClipboardRejectFileRead(clipboard, wire.requestID, + result == LG_CLIPBOARD_RESULT_BLOCKED ? + LG_CLIPBOARD_FILE_ERROR_IO : LG_CLIPBOARD_FILE_ERROR_INVALID); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; + } + + LG_LOCK(clipboard->stateLock); + transfer = networkClipboardFileTransferTakeWire( + &clipboard->fileReads, wire.requestID); + LG_UNLOCK(clipboard->stateLock); + free(transfer); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; +} + +static enum NetworkClipboardProcessResult +networkClipboardProcessFileReady(NetworkClipboard * clipboard, + const struct NetworkClipboardInbound * packet) +{ + LGNetFileTransfer wire; + if (packet->envelope.flags & LG_NET_ENVELOPE_RESPONSE || + lgNetFileTransferDecode(&wire, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + wire.requestID != packet->envelope.requestID || wire.processedLength) + return NETWORK_CLIPBOARD_PROCESS_INVALID; + + uint64_t localRequest = 0; + LG_LOCK(clipboard->stateLock); + if (networkClipboardEpochCurrentNL(clipboard, packet)) + { + struct NetworkFileTransfer * transfer = + networkClipboardFileTransferFindWire( + clipboard->fileWrites, wire.requestID); + if (transfer && transfer->request.dataset == wire.offerID && + transfer->entryID == wire.entryID && transfer->blocked) + { + transfer->blocked = false; + localRequest = transfer->request.request; + } + } + LG_UNLOCK(clipboard->stateLock); + + if (localRequest) + networkClipboardDispatchFileReady(clipboard, localRequest); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; +} + +static enum NetworkClipboardProcessResult networkClipboardProcessFileCancel( + NetworkClipboard * clipboard, + const struct NetworkClipboardInbound * packet) +{ + LGNetFileTransfer wire; + if (lgNetFileTransferDecode(&wire, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + wire.requestID != packet->envelope.requestID) + return NETWORK_CLIPBOARD_PROCESS_INVALID; + + LG_ClipboardFileRequest readRequest = { 0 }; + LG_ClipboardFileRequest writeRequest = { 0 }; + LG_LOCK(clipboard->stateLock); + if (networkClipboardEpochCurrentNL(clipboard, packet)) + { + struct NetworkFileTransfer * read = + networkClipboardFileTransferFindWire( + clipboard->fileReads, wire.requestID); + struct NetworkFileTransfer * write = + networkClipboardFileTransferFindWire( + clipboard->fileWrites, wire.requestID); + if (read && read->request.dataset == wire.offerID && + read->entryID == wire.entryID) + { + read = networkClipboardFileTransferTakeWire( + &clipboard->fileReads, wire.requestID); + readRequest = read->request; + } + else + read = NULL; + if (write && write->request.dataset == wire.offerID && + write->entryID == wire.entryID) + { + write = networkClipboardFileTransferTakeWire( + &clipboard->fileWrites, wire.requestID); + writeRequest = write->request; + } + else + write = NULL; + free(read); + free(write); + } + LG_UNLOCK(clipboard->stateLock); + + if (readRequest.request) + networkClipboardDispatchFileCancel(clipboard, + readRequest.dataset, readRequest.request, + LG_CLIPBOARD_FILE_ERROR_CANCELLED); + if (writeRequest.request) + networkClipboardDispatchFileCancel(clipboard, + writeRequest.dataset, writeRequest.request, + LG_CLIPBOARD_FILE_ERROR_CANCELLED); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; +} + +static enum NetworkClipboardProcessResult networkClipboardProcessFileStatus( + NetworkClipboard * clipboard, + const struct NetworkClipboardInbound * packet) +{ + LGNetFileStatus status; + if (!(packet->envelope.flags & LG_NET_ENVELOPE_RESPONSE) || + lgNetFileStatusDecode(&status, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + status.requestID != packet->envelope.requestID) + return NETWORK_CLIPBOARD_PROCESS_INVALID; + + const LG_ClipboardFileError error = + networkClipboardFileErrorFromStatus(status.status, status.detail); + uint64_t acquisition = 0; + LG_ClipboardFileRequest readRequest = { 0 }; + LG_ClipboardFileRequest writeRequest = { 0 }; + + LG_LOCK(clipboard->stateLock); + if (networkClipboardEpochCurrentNL(clipboard, packet)) + { + if (!status.entryID && status.status != LG_NET_FILE_STATUS_OK) + { + struct NetworkFileAcquisition * item = + networkClipboardAcquisitionFindWire( + clipboard, status.offerID, status.requestID, false); + if (item) + { + acquisition = item->localAcquisition; + item = networkClipboardAcquisitionTake(clipboard, + status.offerID, acquisition, false); + free(item); + } + } + else if (status.status != LG_NET_FILE_STATUS_OK) + { + struct NetworkFileTransfer * read = + networkClipboardFileTransferFindWire( + clipboard->fileReads, status.requestID); + struct NetworkFileTransfer * write = + networkClipboardFileTransferFindWire( + clipboard->fileWrites, status.requestID); + if (read && read->request.dataset == status.offerID && + read->entryID == status.entryID) + { + read = networkClipboardFileTransferTakeWire( + &clipboard->fileReads, status.requestID); + readRequest = read->request; + } + else + read = NULL; + if (write && write->request.dataset == status.offerID && + write->entryID == status.entryID) + { + write = networkClipboardFileTransferTakeWire( + &clipboard->fileWrites, status.requestID); + writeRequest = write->request; + } + else + write = NULL; + free(read); + free(write); + } + } + LG_UNLOCK(clipboard->stateLock); + + if (acquisition) + networkClipboardDispatchFileAcquired( + clipboard, status.offerID, acquisition, error); + if (readRequest.request) + networkClipboardDispatchFileCancel( + clipboard, readRequest.dataset, readRequest.request, error); + if (writeRequest.request) + networkClipboardDispatchFileCancel( + clipboard, writeRequest.dataset, writeRequest.request, error); + return NETWORK_CLIPBOARD_PROCESS_CONSUMED; +} + +static enum NetworkClipboardProcessResult networkClipboardProcessPacket( + NetworkClipboard * clipboard, + const struct NetworkClipboardInbound * packet) +{ + if (packet->envelope.service == LG_NET_SERVICE_CLIPBOARD) + { + if (!networkClipboardEnvelopeValid(packet, LG_NET_SERVICE_CLIPBOARD)) + return NETWORK_CLIPBOARD_PROCESS_INVALID; + + switch (packet->envelope.messageType) + { + case LG_NET_CLIPBOARD_MESSAGE_CLAIM_RESULT: + return networkClipboardProcessClaimResult(clipboard, packet); + + case LG_NET_CLIPBOARD_MESSAGE_KEEPALIVE: + { + LGNetClipboardControl control; + return lgNetClipboardControlDecode(&control, + packet->payload, packet->payloadLength) == LG_NET_PARSE_OK ? + NETWORK_CLIPBOARD_PROCESS_CONSUMED : + NETWORK_CLIPBOARD_PROCESS_INVALID; + } + + case LG_NET_CLIPBOARD_MESSAGE_RELEASE: + case LG_NET_CLIPBOARD_MESSAGE_CLEAR: + return networkClipboardProcessClear(clipboard, packet); + + case LG_NET_CLIPBOARD_MESSAGE_OFFER: + return networkClipboardProcessOffer(clipboard, packet); + + case LG_NET_CLIPBOARD_MESSAGE_REQUEST: + return networkClipboardProcessRequest(clipboard, packet); + + case LG_NET_CLIPBOARD_MESSAGE_DATA_BEGIN: + return networkClipboardProcessDataBegin(clipboard, packet); + + case LG_NET_CLIPBOARD_MESSAGE_DATA_CHUNK: + return networkClipboardProcessDataChunk(clipboard, packet); + + case LG_NET_CLIPBOARD_MESSAGE_DATA_END: + return networkClipboardProcessDataEnd(clipboard, packet); + + case LG_NET_CLIPBOARD_MESSAGE_DATA_READY: + return networkClipboardProcessDataReadyMessage(clipboard, packet); + + case LG_NET_CLIPBOARD_MESSAGE_CANCEL: + return networkClipboardProcessCancel(clipboard, packet); + + case LG_NET_CLIPBOARD_MESSAGE_STATUS: + return networkClipboardProcessStatus(clipboard, packet); + + case LG_NET_CLIPBOARD_MESSAGE_CLAIM: + default: + return NETWORK_CLIPBOARD_PROCESS_INVALID; + } + } + + if (packet->envelope.service != LG_NET_SERVICE_FILE || + !networkClipboardEnvelopeValid(packet, LG_NET_SERVICE_FILE)) + return NETWORK_CLIPBOARD_PROCESS_INVALID; + + switch (packet->envelope.messageType) + { + case LG_NET_FILE_MESSAGE_OFFER: + return networkClipboardProcessFileOffer(clipboard, packet); + + case LG_NET_FILE_MESSAGE_ACQUIRE: + return networkClipboardProcessFileAcquire(clipboard, packet); + + case LG_NET_FILE_MESSAGE_ACQUIRED: + return networkClipboardProcessFileAcquired(clipboard, packet); + + case LG_NET_FILE_MESSAGE_RELEASE: + return networkClipboardProcessFileReleaseMessage(clipboard, packet); + + case LG_NET_FILE_MESSAGE_REQUEST: + return networkClipboardProcessFileRequestMessage(clipboard, packet); + + case LG_NET_FILE_MESSAGE_DATA_BEGIN: + return networkClipboardProcessFileDataBegin(clipboard, packet); + + case LG_NET_FILE_MESSAGE_DATA_CHUNK: + return networkClipboardProcessFileDataChunk(clipboard, packet); + + case LG_NET_FILE_MESSAGE_DATA_END: + return networkClipboardProcessFileDataEnd(clipboard, packet); + + case LG_NET_FILE_MESSAGE_DATA_READY: + return networkClipboardProcessFileReady(clipboard, packet); + + case LG_NET_FILE_MESSAGE_CANCEL: + return networkClipboardProcessFileCancel(clipboard, packet); + + case LG_NET_FILE_MESSAGE_STATUS: + return networkClipboardProcessFileStatus(clipboard, packet); + + case LG_NET_FILE_MESSAGE_ENTRY: + default: + return NETWORK_CLIPBOARD_PROCESS_INVALID; + } +} + +static void networkClipboardProcessInbound(NetworkClipboard * clipboard) +{ + for (unsigned processed = 0; + processed < NETWORK_CLIPBOARD_PROCESS_MAX; ++processed) + { + LG_LOCK(clipboard->stateLock); + struct NetworkClipboardInbound * packet = clipboard->inboundHead; + if (!packet || (clipboard->inboundBlocked && + !clipboard->inboundReady)) + { + LG_UNLOCK(clipboard->stateLock); + return; + } + clipboard->inboundReady = false; + LG_UNLOCK(clipboard->stateLock); + + const enum NetworkClipboardProcessResult result = + networkClipboardProcessPacket(clipboard, packet); + if (result == NETWORK_CLIPBOARD_PROCESS_BLOCKED) + { + LG_LOCK(clipboard->stateLock); + if (clipboard->inboundHead == packet) + clipboard->inboundBlocked = true; + LG_UNLOCK(clipboard->stateLock); + return; + } + + LG_LOCK(clipboard->stateLock); + if (clipboard->inboundHead == packet) + { + clipboard->inboundHead = packet->next; + if (!clipboard->inboundHead) + clipboard->inboundTail = NULL; + --clipboard->inboundCount; + clipboard->inboundBytes -= + sizeof(*packet) + packet->payloadLength; + clipboard->inboundBlocked = false; + clipboard->inboundBlockedRequest = LG_CLIPBOARD_REQUEST_INVALID; + free(packet); + } + LG_UNLOCK(clipboard->stateLock); + + if (result == NETWORK_CLIPBOARD_PROCESS_INVALID) + { + DEBUG_ERROR("Invalid network clipboard service message"); + networkConnectionCancel(clipboard->connection); + return; + } + } +} + +static void networkClipboardSetStatusListener(void * opaque, + LG_ClipboardStatusFn callback, void * callbackOpaque) +{ + NetworkClipboard * clipboard = opaque; + + LG_LOCK(clipboard->statusLock); + LG_LOCK(clipboard->stateLock); + clipboard->statusCallback = callback; + clipboard->statusOpaque = callbackOpaque; + clipboard->statusNotifyPending = false; + + const LG_ClipboardStatus status = networkClipboardStatusNL(clipboard); + LG_UNLOCK(clipboard->stateLock); + + if (callback) + callback(callbackOpaque, &status); + LG_UNLOCK(clipboard->statusLock); +} + +static bool networkClipboardAttach(void * opaque, + const LG_ClipboardEventOps * events, void * eventOpaque) +{ + NetworkClipboard * clipboard = opaque; + if (!events) + return false; + + LG_ClipboardData replay[NETWORK_CLIPBOARD_FORMATS]; + uint64_t dataset = 0; + uint32_t count = 0; + bool files = false; + + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->stateLock); + bool admitted = !clipboard->destroying && clipboard->connected && + clipboard->clipboardService && !clipboard->attached; + if (admitted) + { + clipboard->events = events; + clipboard->eventOpaque = eventOpaque; + clipboard->attached = true; + if (!clipboard->claimPending && !clipboard->claimed && + !networkClipboardQueueClaimNL(clipboard)) + { + clipboard->events = NULL; + clipboard->eventOpaque = NULL; + clipboard->attached = false; + admitted = false; + } + } + + if (admitted && clipboard->remoteOffer.published) + { + dataset = clipboard->remoteOffer.offerID; + count = clipboard->remoteOffer.formatCount; + files = (clipboard->remoteOffer.formatMask & + (UINT32_C(1) << LG_CLIPBOARD_DATA_FILES)) != 0; + memcpy(replay, clipboard->remoteOffer.types, + count * sizeof(*replay)); + } + LG_UNLOCK(clipboard->stateLock); + + if (admitted && count) + { + if (files && events->fileOffer) + events->fileOffer(eventOpaque, dataset); + if (events->notice) + events->notice(eventOpaque, replay, count); + } + LG_UNLOCK(clipboard->eventLock); + + if (admitted) + networkClipboardFlushQueued(clipboard); + return admitted; +} + +static void networkClipboardDetach(void * opaque) +{ + NetworkClipboard * clipboard = opaque; + + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->stateLock); + struct NetworkClipboardTransfer * clipboardReads = + clipboard->clipboardReads; + struct NetworkClipboardTransfer * clipboardWrites = + clipboard->clipboardWrites; + + struct NetworkFileTransfer * fileReads = clipboard->fileReads; + struct NetworkFileTransfer * fileWrites = clipboard->fileWrites; + + struct NetworkFileAcquisition * acquisitions = + clipboard->fileAcquisitions; + clipboard->clipboardReads = NULL; + clipboard->clipboardWrites = NULL; + clipboard->fileReads = NULL; + clipboard->fileWrites = NULL; + clipboard->fileAcquisitions = NULL; + networkClipboardClearSendNL(clipboard); + if (clipboard->attached) + networkClipboardQueueReleaseNL(clipboard); + clipboard->events = NULL; + clipboard->eventOpaque = NULL; + clipboard->attached = false; + clipboard->inboundReady = true; + clipboard->localOffer = (struct NetworkLocalOffer) { 0 }; + networkClipboardRemoteOfferResetNL(clipboard); + LG_UNLOCK(clipboard->stateLock); + LG_UNLOCK(clipboard->eventLock); + + networkClipboardTransfersFree(clipboardReads); + networkClipboardTransfersFree(clipboardWrites); + networkClipboardFileTransfersFree(fileReads); + networkClipboardFileTransfersFree(fileWrites); + networkClipboardAcquisitionsFree(acquisitions); + + networkClipboardFlushQueued(clipboard); +} + +static bool networkClipboardRelease(void * opaque) +{ + NetworkClipboard * clipboard = opaque; + + LG_LOCK(clipboard->stateLock); + if ((!clipboard->claimPending && !clipboard->claimed) || + !clipboard->localOffer.offerID) + { + LG_UNLOCK(clipboard->stateLock); + return false; + } + + const uint64_t offerID = clipboard->localOffer.offerID; + const bool queued = networkClipboardQueueControlNL(clipboard, + LG_NET_CLIPBOARD_MESSAGE_CLEAR, offerID); + if (queued) + clipboard->localOffer = (struct NetworkLocalOffer) { 0 }; + LG_UNLOCK(clipboard->stateLock); + + if (queued) + networkClipboardFlushQueued(clipboard); + return queued; +} + +static bool networkClipboardNotifyTypes(void * opaque, + const LG_ClipboardData types[], size_t count) +{ + NetworkClipboard * clipboard = opaque; + if (!types || !count || count > NETWORK_CLIPBOARD_FORMATS) + return false; + + uint32_t mask = 0; + for (size_t index = 0; index < count; ++index) + { + if (!networkClipboardDataValid(types[index]) || + types[index] == LG_CLIPBOARD_DATA_FILES) + return false; + const uint32_t bit = UINT32_C(1) << types[index]; + if (mask & bit) + return false; + mask |= bit; + } + + LG_LOCK(clipboard->stateLock); + if ((!clipboard->claimPending && !clipboard->claimed) || + !clipboard->attached) + { + LG_UNLOCK(clipboard->stateLock); + return false; + } + + struct NetworkLocalOffer previous = clipboard->localOffer; + memset(&clipboard->localOffer, 0, sizeof(clipboard->localOffer)); + clipboard->localOffer.offerID = + networkClipboardNextLocalIDNL(clipboard, false); + clipboard->localOffer.serial = + networkClipboardNext64(clipboard->remoteSerial); + + clipboard->remoteSerial = clipboard->localOffer.serial; + clipboard->localOffer.count = (uint32_t)count; + + for (uint32_t index = 0; index < count; ++index) + { + clipboard->localOffer.types[index] = types[index]; + clipboard->localOffer.indices[types[index]] = index; + } + + const bool queued = networkClipboardQueueLocalOfferNL(clipboard); + if (!queued) + clipboard->localOffer = previous; + LG_UNLOCK(clipboard->stateLock); + + if (queued) + networkClipboardFlushQueued(clipboard); + return queued; +} + +static bool networkClipboardOfferFiles(void * opaque, uint64_t dataset) +{ + NetworkClipboard * clipboard = opaque; + if (!dataset || kvmfrClipboardTransferFromHelper(dataset)) + return false; + + LG_LOCK(clipboard->stateLock); + if ((!clipboard->claimPending && !clipboard->claimed) || + !clipboard->attached || !clipboard->fileService) + { + LG_UNLOCK(clipboard->stateLock); + return false; + } + + struct NetworkLocalOffer previous = clipboard->localOffer; + memset(&clipboard->localOffer, 0, sizeof(clipboard->localOffer)); + clipboard->localOffer.offerID = dataset; + clipboard->localOffer.serial = + networkClipboardNext64(clipboard->remoteSerial); + + clipboard->remoteSerial = clipboard->localOffer.serial; + + clipboard->localOffer.count = 1; + clipboard->localOffer.types[0] = + LG_CLIPBOARD_DATA_FILES; + clipboard->localOffer.indices[LG_CLIPBOARD_DATA_FILES] = 0; + clipboard->localOffer.files = true; + + const bool queued = networkClipboardQueueLocalOfferNL(clipboard); + if (!queued) + clipboard->localOffer = previous; + LG_UNLOCK(clipboard->stateLock); + + if (queued) + networkClipboardFlushQueued(clipboard); + return queued; +} + +static LG_ClipboardResult networkClipboardDataBegin(void * opaque, + LG_ClipboardRequest request, LG_ClipboardData type, uint64_t sizeHint) +{ + NetworkClipboard * clipboard = opaque; + if (!kvmfrClipboardTransferFromHelper(request) || + !networkClipboardDataValid(type) || type == LG_CLIPBOARD_DATA_FILES) + return LG_CLIPBOARD_RESULT_FAILED; + + LG_LOCK(clipboard->stateLock); + struct NetworkClipboardTransfer * transfer = + networkClipboardTransferFind(clipboard->clipboardWrites, request); + if (!transfer || transfer->began || transfer->type != type) + { + LG_UNLOCK(clipboard->stateLock); + return LG_CLIPBOARD_RESULT_FAILED; + } + + transfer->delivery = clipboard->clipboardBulk ? + NETWORK_PACKET_DELIVERY_BULK : NETWORK_PACKET_DELIVERY_RELIABLE; + + const LGNetClipboardTransfer wire = + { + .requestID = transfer->wireRequest, + .offerID = transfer->offerID, + .totalLength = sizeHint, + .processedLength = 0, + }; + struct NetworkClipboardSend * send = networkClipboardSendWithDelivery( + networkClipboardTransferSendNew(clipboard, + LG_NET_CLIPBOARD_MESSAGE_DATA_BEGIN, &wire, 0), + transfer->delivery); + if (!send) + { + LG_UNLOCK(clipboard->stateLock); + return LG_CLIPBOARD_RESULT_FAILED; + } + + if (!networkClipboardQueueSendNL(clipboard, send)) + { + transfer->blocked = true; + LG_UNLOCK(clipboard->stateLock); + return LG_CLIPBOARD_RESULT_BLOCKED; + } + + transfer->began = true; + transfer->blocked = false; + transfer->sizeHint = sizeHint; + transfer->offset = 0; + LG_UNLOCK(clipboard->stateLock); + networkClipboardFlushQueued(clipboard); + return LG_CLIPBOARD_RESULT_ACCEPTED; +} + +static LG_ClipboardResult networkClipboardDataChunk(void * opaque, + LG_ClipboardRequest request, uint64_t offset, + const void * data, size_t size) +{ + NetworkClipboard * clipboard = opaque; + if (!kvmfrClipboardTransferFromHelper(request) || !data || !size || + size > NETWORK_CLIPBOARD_EMITTED_CHUNK || size > UINT32_MAX || + offset > UINT64_MAX - size) + return LG_CLIPBOARD_RESULT_FAILED; + + LG_LOCK(clipboard->stateLock); + struct NetworkClipboardTransfer * transfer = + networkClipboardTransferFind(clipboard->clipboardWrites, request); + if (!transfer || !transfer->began || transfer->offset != offset || + size > transfer->maxChunkLength || + (transfer->sizeHint != LG_CLIPBOARD_SIZE_UNKNOWN && + offset + size > transfer->sizeHint)) + { + LG_UNLOCK(clipboard->stateLock); + return LG_CLIPBOARD_RESULT_FAILED; + } + + LGNetClipboardChunkFlags flags = offset ? 0 : + LG_NET_CLIPBOARD_CHUNK_FIRST; + if (transfer->sizeHint != LG_CLIPBOARD_SIZE_UNKNOWN && + offset + size == transfer->sizeHint) + flags |= LG_NET_CLIPBOARD_CHUNK_FINAL; + const LGNetClipboardChunk chunk = + { + .requestID = transfer->wireRequest, + .offerID = transfer->offerID, + .offset = offset, + .totalLength = transfer->sizeHint, + .dataLength = (uint32_t)size, + .flags = flags, + .data = data, + }; + struct NetworkClipboardSend * send = + networkClipboardSendWithDelivery( + networkClipboardChunkSendNew(clipboard, &chunk), + transfer->delivery); + if (!send) + { + LG_UNLOCK(clipboard->stateLock); + return LG_CLIPBOARD_RESULT_FAILED; + } + + if (!networkClipboardQueueSendNL(clipboard, send)) + { + transfer->blocked = true; + LG_UNLOCK(clipboard->stateLock); + return LG_CLIPBOARD_RESULT_BLOCKED; + } + + transfer->offset += size; + transfer->blocked = false; + LG_UNLOCK(clipboard->stateLock); + networkClipboardFlushQueued(clipboard); + return LG_CLIPBOARD_RESULT_ACCEPTED; +} + +static LG_ClipboardResult networkClipboardDataEnd(void * opaque, + LG_ClipboardRequest request, uint64_t finalSize) +{ + NetworkClipboard * clipboard = opaque; + if (!kvmfrClipboardTransferFromHelper(request)) + return LG_CLIPBOARD_RESULT_FAILED; + + LG_LOCK(clipboard->stateLock); + struct NetworkClipboardTransfer * transfer = + networkClipboardTransferFind(clipboard->clipboardWrites, request); + if (!transfer || !transfer->began || transfer->offset != finalSize || + (transfer->sizeHint != LG_CLIPBOARD_SIZE_UNKNOWN && + transfer->sizeHint != finalSize)) + { + LG_UNLOCK(clipboard->stateLock); + return LG_CLIPBOARD_RESULT_FAILED; + } + + const LGNetClipboardTransfer wire = + { + .requestID = transfer->wireRequest, + .offerID = transfer->offerID, + .totalLength = finalSize, + .processedLength = finalSize, + }; + struct NetworkClipboardSend * send = networkClipboardSendWithDelivery( + networkClipboardTransferSendNew(clipboard, + LG_NET_CLIPBOARD_MESSAGE_DATA_END, &wire, 0), + transfer->delivery); + if (!send) + { + LG_UNLOCK(clipboard->stateLock); + return LG_CLIPBOARD_RESULT_FAILED; + } + if (!networkClipboardQueueSendNL(clipboard, send)) + { + transfer->blocked = true; + LG_UNLOCK(clipboard->stateLock); + return LG_CLIPBOARD_RESULT_BLOCKED; + } + + transfer = networkClipboardTransferTake( + &clipboard->clipboardWrites, request); + LG_UNLOCK(clipboard->stateLock); + free(transfer); + networkClipboardFlushQueued(clipboard); + return LG_CLIPBOARD_RESULT_ACCEPTED; +} + +static bool networkClipboardData(void * opaque, + LG_ClipboardRequest request, LG_ClipboardData type, + const void * data, size_t size) +{ + NetworkClipboard * clipboard = opaque; + if (!kvmfrClipboardTransferFromHelper(request) || + (type != LG_CLIPBOARD_DATA_NONE && + (!networkClipboardDataValid(type) || + type == LG_CLIPBOARD_DATA_FILES)) || + (size && !data) || (type == LG_CLIPBOARD_DATA_NONE && size)) + return false; + + LG_LOCK(clipboard->stateLock); + struct NetworkClipboardTransfer * transfer = + networkClipboardTransferFind(clipboard->clipboardWrites, request); + if (!transfer || transfer->began || + (type != LG_CLIPBOARD_DATA_NONE && transfer->type != type)) + { + LG_UNLOCK(clipboard->stateLock); + return false; + } + + if (type == LG_CLIPBOARD_DATA_NONE) + { + const bool queued = networkClipboardQueueClipboardStatusNL(clipboard, + transfer->wireRequest, transfer->offerID, + LG_NET_CLIPBOARD_STATUS_UNSUPPORTED, 0, 0); + if (queued) + transfer = networkClipboardTransferTake( + &clipboard->clipboardWrites, request); + LG_UNLOCK(clipboard->stateLock); + if (queued) + { + free(transfer); + networkClipboardFlushQueued(clipboard); + } + return queued; + } + + const size_t maxChunkLength = transfer->maxChunkLength < + NETWORK_CLIPBOARD_EMITTED_CHUNK ? transfer->maxChunkLength : + NETWORK_CLIPBOARD_EMITTED_CHUNK; + const size_t chunkCount = size ? + 1U + (size - 1U) / maxChunkLength : 0; + if (chunkCount > NETWORK_CLIPBOARD_PENDING_MAX - 2U) + { + LG_UNLOCK(clipboard->stateLock); + return false; + } + + const size_t fixedBytes = + 2U * (sizeof(struct NetworkClipboardSend) + + LG_NET_CLIPBOARD_TRANSFER_WIRE_SIZE) + + chunkCount * (sizeof(struct NetworkClipboardSend) + + LG_NET_CLIPBOARD_CHUNK_HEADER_WIRE_SIZE); + if (fixedBytes > NETWORK_CLIPBOARD_PENDING_BYTES || + size > NETWORK_CLIPBOARD_PENDING_BYTES - fixedBytes) + { + LG_UNLOCK(clipboard->stateLock); + return false; + } + + const NetworkPacketDelivery delivery = clipboard->clipboardBulk ? + NETWORK_PACKET_DELIVERY_BULK : NETWORK_PACKET_DELIVERY_RELIABLE; + struct NetworkClipboardBatch batch = { 0 }; + const LGNetClipboardTransfer begin = + { + .requestID = transfer->wireRequest, + .offerID = transfer->offerID, + .totalLength = size, + .processedLength = 0, + }; + struct NetworkClipboardSend * send = networkClipboardSendWithDelivery( + networkClipboardTransferSendNew(clipboard, + LG_NET_CLIPBOARD_MESSAGE_DATA_BEGIN, &begin, 0), delivery); + if (!send) + goto fail; + networkClipboardBatchAppend(&batch, send); + + size_t offset = 0; + while (offset < size) + { + size_t chunkLength = size - offset; + if (chunkLength > transfer->maxChunkLength) + chunkLength = transfer->maxChunkLength; + if (chunkLength > NETWORK_CLIPBOARD_EMITTED_CHUNK) + chunkLength = NETWORK_CLIPBOARD_EMITTED_CHUNK; + + const bool final = offset + chunkLength == size; + const LGNetClipboardChunk chunk = + { + .requestID = transfer->wireRequest, + .offerID = transfer->offerID, + .offset = offset, + .totalLength = size, + .dataLength = (uint32_t)chunkLength, + .flags = (offset ? 0 : LG_NET_CLIPBOARD_CHUNK_FIRST) | + (final ? LG_NET_CLIPBOARD_CHUNK_FINAL : 0), + .data = (const uint8_t *)data + offset, + }; + send = networkClipboardSendWithDelivery( + networkClipboardChunkSendNew(clipboard, &chunk), delivery); + if (!send) + goto fail; + networkClipboardBatchAppend(&batch, send); + offset += chunkLength; + } + + const LGNetClipboardTransfer end = + { + .requestID = transfer->wireRequest, + .offerID = transfer->offerID, + .totalLength = size, + .processedLength = size, + }; + send = networkClipboardSendWithDelivery( + networkClipboardTransferSendNew(clipboard, + LG_NET_CLIPBOARD_MESSAGE_DATA_END, &end, 0), delivery); + if (!send) + goto fail; + networkClipboardBatchAppend(&batch, send); + + if (!networkClipboardQueueBatchNL(clipboard, &batch)) + goto fail; + transfer = networkClipboardTransferTake( + &clipboard->clipboardWrites, request); + LG_UNLOCK(clipboard->stateLock); + free(transfer); + networkClipboardFlushQueued(clipboard); + return true; + +fail: + networkClipboardBatchFree(&batch); + LG_UNLOCK(clipboard->stateLock); + return false; +} + +static bool networkClipboardDataCancel(void * opaque, + LG_ClipboardRequest request, LG_ClipboardCancelReason reason) +{ + NetworkClipboard * clipboard = opaque; + if (!kvmfrClipboardTransferFromHelper(request) || + reason > LG_CLIPBOARD_CANCEL_INVALID) + return false; + + LG_LOCK(clipboard->stateLock); + struct NetworkClipboardTransfer * transfer = + networkClipboardTransferFind(clipboard->clipboardWrites, request); + if (!transfer) + { + LG_UNLOCK(clipboard->stateLock); + return false; + } + + const LGNetClipboardTransfer wire = + { + .requestID = transfer->wireRequest, + .offerID = transfer->offerID, + .totalLength = transfer->sizeHint, + .processedLength = transfer->offset, + }; + struct NetworkClipboardSend * send = networkClipboardTransferSendNew( + clipboard, LG_NET_CLIPBOARD_MESSAGE_CANCEL, &wire, 0); + const bool queued = networkClipboardQueueSendNL(clipboard, send); + if (queued) + transfer = networkClipboardTransferTake( + &clipboard->clipboardWrites, request); + LG_UNLOCK(clipboard->stateLock); + + if (queued) + { + free(transfer); + networkClipboardFlushQueued(clipboard); + } + return queued; +} + +static bool networkClipboardDataReady(void * opaque, + LG_ClipboardRequest request) +{ + NetworkClipboard * clipboard = opaque; + if (!request) + return false; + + LG_LOCK(clipboard->stateLock); + struct NetworkClipboardTransfer * transfer = + networkClipboardTransferFind(clipboard->clipboardReads, request); + if (!transfer || !clipboard->inboundBlocked || + clipboard->inboundBlockedRequest != request) + { + LG_UNLOCK(clipboard->stateLock); + return false; + } + + clipboard->inboundReady = true; + LG_UNLOCK(clipboard->stateLock); + + networkClipboardFlushQueued(clipboard); + return true; +} + +static bool networkClipboardRequest(void * opaque, + LG_ClipboardRequest request, LG_ClipboardData type) +{ + NetworkClipboard * clipboard = opaque; + if (!request || kvmfrClipboardTransferFromHelper(request) || + !networkClipboardDataValid(type) || type == LG_CLIPBOARD_DATA_FILES) + return false; + + struct NetworkClipboardTransfer * transfer = calloc(1, sizeof(*transfer)); + if (!transfer) + return false; + + LG_LOCK(clipboard->stateLock); + const struct NetworkRemoteOffer * offer = &clipboard->remoteOffer; + if (!offer->published || offer->indices[type] == UINT32_MAX || + networkClipboardTransferCount(clipboard->clipboardReads) >= + NETWORK_CLIPBOARD_FILE_REQUESTS || + networkClipboardTransferFind(clipboard->clipboardReads, request)) + { + LG_UNLOCK(clipboard->stateLock); + free(transfer); + return false; + } + + transfer->localRequest = request; + transfer->wireRequest = networkClipboardNextWireIDNL(clipboard); + transfer->offerID = offer->offerID; + transfer->type = type; + transfer->formatIndex = offer->indices[type]; + transfer->maxChunkLength = NETWORK_CLIPBOARD_EMITTED_CHUNK; + + const LGNetClipboardRequest wire = + { + .offerID = transfer->offerID, + .requestID = transfer->wireRequest, + .formatIndex = transfer->formatIndex, + .maxChunkLength = transfer->maxChunkLength, + .offset = 0, + }; + struct NetworkClipboardSend * send = + networkClipboardRequestSendNew(clipboard, &wire); + if (!send || !networkClipboardQueueSendNL(clipboard, send)) + { + LG_UNLOCK(clipboard->stateLock); + free(transfer); + return false; + } + + transfer->sizeHint = LG_CLIPBOARD_SIZE_UNKNOWN; + transfer->next = clipboard->clipboardReads; + clipboard->clipboardReads = transfer; + LG_UNLOCK(clipboard->stateLock); + networkClipboardFlushQueued(clipboard); + return true; +} + +static bool networkClipboardFileAcquire(void * opaque, uint64_t dataset, + uint64_t acquisitionID) +{ + NetworkClipboard * clipboard = opaque; + if (!dataset || !kvmfrClipboardTransferFromClient(acquisitionID)) + return false; + + struct NetworkFileAcquisition * acquisition = + calloc(1, sizeof(*acquisition)); + if (!acquisition) + return false; + + LG_LOCK(clipboard->stateLock); + if (!clipboard->remoteOffer.published || + !clipboard->remoteOffer.fileOffer || + clipboard->remoteOffer.offerID != dataset || + networkClipboardAcquisitionCountNL(clipboard) >= + NETWORK_CLIPBOARD_FILE_ACQUISITIONS || + networkClipboardAcquisitionFind( + clipboard, dataset, acquisitionID, false)) + { + LG_UNLOCK(clipboard->stateLock); + free(acquisition); + return false; + } + + acquisition->dataset = dataset; + acquisition->localAcquisition = acquisitionID; + acquisition->wireClientID = networkClipboardNextWireIDNL(clipboard); + acquisition->fromRemote = false; + + const LGNetFileLease lease = + { + .clientID = acquisition->wireClientID, + .offerID = dataset, + .leaseEpoch = 0, + .leaseMs = LG_NET_FILE_MAX_LEASE_MS, + .flags = LG_NET_FILE_LEASE_READ, + }; + struct NetworkClipboardSend * send = networkClipboardFileLeaseSendNew( + clipboard, LG_NET_FILE_MESSAGE_ACQUIRE, &lease, + LG_NET_ENVELOPE_ACK_REQUIRED); + if (!send || !networkClipboardQueueSendNL(clipboard, send)) + { + LG_UNLOCK(clipboard->stateLock); + free(acquisition); + return false; + } + + acquisition->next = clipboard->fileAcquisitions; + clipboard->fileAcquisitions = acquisition; + LG_UNLOCK(clipboard->stateLock); + networkClipboardFlushQueued(clipboard); + return true; +} + +static bool networkClipboardFileAcquired(void * opaque, uint64_t dataset, + uint64_t acquisitionID, LG_ClipboardFileError error) +{ + NetworkClipboard * clipboard = opaque; + if (!dataset || !kvmfrClipboardTransferFromHelper(acquisitionID) || + !networkClipboardFileErrorValid(error)) + return false; + + LG_LOCK(clipboard->stateLock); + struct NetworkFileAcquisition * acquisition = + networkClipboardAcquisitionFind( + clipboard, dataset, acquisitionID, true); + if (!acquisition || acquisition->active) + { + LG_UNLOCK(clipboard->stateLock); + return false; + } + + bool queued; + if (error == LG_CLIPBOARD_FILE_ERROR_NONE) + { + const uint64_t leaseEpoch = + networkClipboardNextWireIDNL(clipboard); + const LGNetFileLease lease = + { + .clientID = acquisition->wireClientID, + .offerID = dataset, + .leaseEpoch = leaseEpoch, + .leaseMs = LG_NET_FILE_MAX_LEASE_MS, + .flags = LG_NET_FILE_LEASE_READ | + LG_NET_FILE_LEASE_ACQUIRED, + }; + queued = networkClipboardQueueSendNL(clipboard, + networkClipboardFileLeaseSendNew(clipboard, + LG_NET_FILE_MESSAGE_ACQUIRED, &lease, + LG_NET_ENVELOPE_RESPONSE)); + if (queued) + { + acquisition->leaseEpoch = leaseEpoch; + acquisition->active = true; + } + } + else + { + queued = networkClipboardQueueFileStatusNL(clipboard, + acquisition->wireClientID, dataset, 0, error, 0); + if (queued) + acquisition = networkClipboardAcquisitionTake( + clipboard, dataset, acquisitionID, true); + } + LG_UNLOCK(clipboard->stateLock); + + if (queued) + { + if (error != LG_CLIPBOARD_FILE_ERROR_NONE) + free(acquisition); + networkClipboardFlushQueued(clipboard); + } + return queued; +} + +static bool networkClipboardFileRelease(void * opaque, uint64_t dataset, + uint64_t acquisitionID) +{ + NetworkClipboard * clipboard = opaque; + if (!dataset || !kvmfrClipboardTransferFromClient(acquisitionID)) + return false; + + LG_LOCK(clipboard->stateLock); + struct NetworkFileAcquisition * acquisition = + networkClipboardAcquisitionFind( + clipboard, dataset, acquisitionID, false); + if (!acquisition || !acquisition->active) + { + LG_UNLOCK(clipboard->stateLock); + return false; + } + + const LGNetFileLease lease = + { + .clientID = acquisition->wireClientID, + .offerID = dataset, + .leaseEpoch = acquisition->leaseEpoch, + .leaseMs = 0, + .flags = LG_NET_FILE_LEASE_READ, + }; + const bool queued = networkClipboardQueueSendNL(clipboard, + networkClipboardFileLeaseSendNew(clipboard, + LG_NET_FILE_MESSAGE_RELEASE, &lease, 0)); + if (queued) + acquisition = networkClipboardAcquisitionTake( + clipboard, dataset, acquisitionID, false); + LG_UNLOCK(clipboard->stateLock); + + if (queued) + { + free(acquisition); + networkClipboardFlushQueued(clipboard); + } + return queued; +} + +static bool networkClipboardFileRequest(void * opaque, + const LG_ClipboardFileRequest * descriptor) +{ + NetworkClipboard * clipboard = opaque; + if (!descriptor || !descriptor->dataset || + !kvmfrClipboardTransferFromClient(descriptor->request) || + (descriptor->operation != LG_CLIPBOARD_FILE_LIST && + descriptor->operation != LG_CLIPBOARD_FILE_READ) || + (descriptor->operation == LG_CLIPBOARD_FILE_LIST && + (descriptor->offset || descriptor->length)) || + (descriptor->operation == LG_CLIPBOARD_FILE_READ && + (!descriptor->length || + descriptor->length > NETWORK_CLIPBOARD_FILE_READ_MAX || + descriptor->offset > UINT64_MAX - descriptor->length))) + return false; + + uint64_t entryID; + if (!lgNetFileEntryIDFromKVMFRNode(descriptor->node, &entryID)) + return false; + + struct NetworkFileTransfer * transfer = calloc(1, sizeof(*transfer)); + if (!transfer) + return false; + + LG_LOCK(clipboard->stateLock); + if (!networkClipboardDatasetAcquiredNL( + clipboard, descriptor->dataset, false) || + networkClipboardFileTransferCountNL(clipboard) >= + NETWORK_CLIPBOARD_FILE_REQUESTS || + networkClipboardFileTransferFind( + clipboard->fileReads, descriptor->request)) + { + LG_UNLOCK(clipboard->stateLock); + free(transfer); + return false; + } + + transfer->request = *descriptor; + transfer->wireRequest = networkClipboardNextWireIDNL(clipboard); + transfer->entryID = entryID; + transfer->sizeHint = LG_CLIPBOARD_SIZE_UNKNOWN; + + const LGNetFileRequest request = + { + .requestID = transfer->wireRequest, + .offerID = descriptor->dataset, + .entryID = entryID, + .offset = descriptor->offset, + .length = descriptor->length, + .flags = descriptor->operation == LG_CLIPBOARD_FILE_READ ? + LG_NET_FILE_REQUEST_DATA : LG_NET_FILE_REQUEST_METADATA, + }; + struct NetworkClipboardSend * send = + networkClipboardFileRequestSendNew(clipboard, &request); + if (!send || !networkClipboardQueueSendNL(clipboard, send)) + { + LG_UNLOCK(clipboard->stateLock); + free(transfer); + return false; + } + + transfer->next = clipboard->fileReads; + clipboard->fileReads = transfer; + LG_UNLOCK(clipboard->stateLock); + networkClipboardFlushQueued(clipboard); + return true; +} + +static LG_ClipboardResult networkClipboardFileDataBegin(void * opaque, + const LG_ClipboardFileRequest * descriptor, uint64_t sizeHint) +{ + NetworkClipboard * clipboard = opaque; + if (!descriptor || !kvmfrClipboardTransferFromHelper( + descriptor->request) || + descriptor->offset > UINT64_MAX - sizeHint) + return LG_CLIPBOARD_RESULT_FAILED; + + LG_LOCK(clipboard->stateLock); + struct NetworkFileTransfer * transfer = + networkClipboardFileTransferFind( + clipboard->fileWrites, descriptor->request); + if (!transfer || transfer->began || + !networkClipboardSameFileRequest(&transfer->request, descriptor) || + (descriptor->operation == LG_CLIPBOARD_FILE_READ && + (sizeHint > NETWORK_CLIPBOARD_FILE_READ_MAX || + sizeHint > descriptor->length))) + { + LG_UNLOCK(clipboard->stateLock); + return LG_CLIPBOARD_RESULT_FAILED; + } + + transfer->delivery = clipboard->fileBulk ? + NETWORK_PACKET_DELIVERY_BULK : NETWORK_PACKET_DELIVERY_RELIABLE; + + const LGNetFileTransfer wire = + { + .requestID = transfer->wireRequest, + .offerID = descriptor->dataset, + .entryID = transfer->entryID, + .offset = descriptor->offset, + .totalLength = descriptor->offset + sizeHint, + .processedLength = 0, + }; + struct NetworkClipboardSend * send = networkClipboardSendWithDelivery( + networkClipboardFileTransferSendNew(clipboard, + LG_NET_FILE_MESSAGE_DATA_BEGIN, &wire, 0), transfer->delivery); + if (!send) + { + LG_UNLOCK(clipboard->stateLock); + return LG_CLIPBOARD_RESULT_FAILED; + } + if (!networkClipboardQueueSendNL(clipboard, send)) + { + transfer->blocked = true; + LG_UNLOCK(clipboard->stateLock); + return LG_CLIPBOARD_RESULT_BLOCKED; + } + + transfer->began = true; + transfer->blocked = false; + transfer->sizeHint = sizeHint; + transfer->responseOffset = 0; + transfer->wireEnded = false; + LG_UNLOCK(clipboard->stateLock); + networkClipboardFlushQueued(clipboard); + return LG_CLIPBOARD_RESULT_ACCEPTED; +} + +static LG_ClipboardResult networkClipboardFileDataChunk(void * opaque, + const LG_ClipboardFileRequest * descriptor, uint64_t responseOffset, + const void * data, size_t size, bool end) +{ + NetworkClipboard * clipboard = opaque; + if (!descriptor || !kvmfrClipboardTransferFromHelper( + descriptor->request) || !data || !size || + size > NETWORK_CLIPBOARD_EMITTED_CHUNK || size > UINT32_MAX || + responseOffset > UINT64_MAX - size) + return LG_CLIPBOARD_RESULT_FAILED; + + LG_LOCK(clipboard->stateLock); + struct NetworkFileTransfer * transfer = + networkClipboardFileTransferFind( + clipboard->fileWrites, descriptor->request); + if (!transfer || !transfer->began || transfer->wireEnded || + !networkClipboardSameFileRequest(&transfer->request, descriptor) || + transfer->responseOffset != responseOffset || + responseOffset + size > transfer->sizeHint || + (end && responseOffset + size != transfer->sizeHint) || + descriptor->offset > UINT64_MAX - transfer->sizeHint) + { + LG_UNLOCK(clipboard->stateLock); + return LG_CLIPBOARD_RESULT_FAILED; + } + + const bool final = responseOffset + size == transfer->sizeHint; + LGNetFileChunkFlags flags = responseOffset ? 0 : + LG_NET_FILE_CHUNK_FIRST; + if (final) + flags |= LG_NET_FILE_CHUNK_FINAL; + const LGNetFileChunk chunk = + { + .requestID = transfer->wireRequest, + .offerID = descriptor->dataset, + .entryID = transfer->entryID, + .offset = responseOffset, + .totalLength = transfer->sizeHint, + .dataLength = (uint32_t)size, + .flags = flags, + .data = data, + }; + struct NetworkClipboardSend * send = + networkClipboardSendWithDelivery( + networkClipboardFileChunkSendNew(clipboard, &chunk), + transfer->delivery); + if (!send) + { + LG_UNLOCK(clipboard->stateLock); + return LG_CLIPBOARD_RESULT_FAILED; + } + if (!networkClipboardQueueSendNL(clipboard, send)) + { + transfer->blocked = true; + LG_UNLOCK(clipboard->stateLock); + return LG_CLIPBOARD_RESULT_BLOCKED; + } + + transfer->responseOffset += size; + transfer->blocked = false; + transfer->wireEnded = final; + LG_UNLOCK(clipboard->stateLock); + networkClipboardFlushQueued(clipboard); + return LG_CLIPBOARD_RESULT_ACCEPTED; +} + +static LG_ClipboardResult networkClipboardFileDataEnd(void * opaque, + const LG_ClipboardFileRequest * descriptor, uint64_t finalSize) +{ + NetworkClipboard * clipboard = opaque; + if (!descriptor || !kvmfrClipboardTransferFromHelper( + descriptor->request) || + descriptor->offset > UINT64_MAX - finalSize) + return LG_CLIPBOARD_RESULT_FAILED; + + LG_LOCK(clipboard->stateLock); + struct NetworkFileTransfer * transfer = + networkClipboardFileTransferFind( + clipboard->fileWrites, descriptor->request); + if (!transfer || !transfer->began || + !networkClipboardSameFileRequest(&transfer->request, descriptor) || + transfer->responseOffset != finalSize || + transfer->sizeHint != finalSize) + { + LG_UNLOCK(clipboard->stateLock); + return LG_CLIPBOARD_RESULT_FAILED; + } + + const LGNetFileTransfer wire = + { + .requestID = transfer->wireRequest, + .offerID = descriptor->dataset, + .entryID = transfer->entryID, + .offset = descriptor->offset, + .totalLength = descriptor->offset + finalSize, + .processedLength = finalSize, + }; + struct NetworkClipboardSend * send = networkClipboardSendWithDelivery( + networkClipboardFileTransferSendNew(clipboard, + LG_NET_FILE_MESSAGE_DATA_END, &wire, 0), transfer->delivery); + if (!send) + { + LG_UNLOCK(clipboard->stateLock); + return LG_CLIPBOARD_RESULT_FAILED; + } + if (!networkClipboardQueueSendNL(clipboard, send)) + { + transfer->blocked = true; + LG_UNLOCK(clipboard->stateLock); + return LG_CLIPBOARD_RESULT_BLOCKED; + } + + transfer = networkClipboardFileTransferTake( + &clipboard->fileWrites, descriptor->request); + LG_UNLOCK(clipboard->stateLock); + free(transfer); + networkClipboardFlushQueued(clipboard); + return LG_CLIPBOARD_RESULT_ACCEPTED; +} + +static bool networkClipboardFileCancel(void * opaque, uint64_t dataset, + uint64_t request, LG_ClipboardFileError reason) +{ + NetworkClipboard * clipboard = opaque; + if (!dataset || !request || reason == LG_CLIPBOARD_FILE_ERROR_NONE || + !networkClipboardFileErrorValid(reason)) + return false; + + LG_LOCK(clipboard->stateLock); + struct NetworkFileTransfer * read = + networkClipboardFileTransferFind(clipboard->fileReads, request); + struct NetworkFileTransfer * write = + networkClipboardFileTransferFind(clipboard->fileWrites, request); + + struct NetworkFileAcquisition * acquisition = + networkClipboardAcquisitionFind(clipboard, dataset, request, + kvmfrClipboardTransferFromHelper(request)); + + uint64_t wireRequest = 0; + uint64_t entryID = 0; + uint64_t offset = 0; + uint64_t totalLength = 0; + uint64_t processed = 0; + if (read && read->request.dataset == dataset) + { + wireRequest = read->wireRequest; + entryID = read->entryID; + offset = read->request.offset; + totalLength = read->sizeHint == LG_CLIPBOARD_SIZE_UNKNOWN ? + UINT64_MAX : offset + read->sizeHint; + processed = read->responseOffset; + } + else if (write && write->request.dataset == dataset) + { + wireRequest = write->wireRequest; + entryID = write->entryID; + offset = write->request.offset; + totalLength = offset + write->sizeHint; + processed = write->responseOffset; + } + else if (acquisition) + wireRequest = acquisition->wireClientID; + else + { + LG_UNLOCK(clipboard->stateLock); + return false; + } + + bool queued; + if (entryID) + { + const LGNetFileTransfer wire = + { + .requestID = wireRequest, + .offerID = dataset, + .entryID = entryID, + .offset = offset, + .totalLength = totalLength, + .processedLength = processed, + }; + queued = networkClipboardQueueSendNL(clipboard, + networkClipboardFileTransferSendNew(clipboard, + LG_NET_FILE_MESSAGE_CANCEL, &wire, 0)); + } + else + queued = networkClipboardQueueFileStatusNL(clipboard, + wireRequest, dataset, 0, reason, 0); + + if (queued) + { + free(networkClipboardFileTransferTake(&clipboard->fileReads, request)); + free(networkClipboardFileTransferTake(&clipboard->fileWrites, request)); + if (acquisition) + free(networkClipboardAcquisitionTake(clipboard, + dataset, request, acquisition->fromRemote)); + } + LG_UNLOCK(clipboard->stateLock); + + if (queued) + networkClipboardFlushQueued(clipboard); + return queued; +} + +static void networkClipboardDispatchDisconnected( + NetworkClipboard * clipboard, + struct NetworkClipboardTransfer * clipboardReads, + struct NetworkClipboardTransfer * clipboardWrites, + struct NetworkFileTransfer * fileReads, + struct NetworkFileTransfer * fileWrites, + struct NetworkFileAcquisition * acquisitions, + bool release) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->stateLock); + const LG_ClipboardEventOps * events = clipboard->attached ? + clipboard->events : NULL; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->stateLock); + + if (events) + { + for (const struct NetworkClipboardTransfer * item = clipboardReads; + item; item = item->next) + if (events->dataCancel) + events->dataCancel(opaque, item->localRequest, + LG_CLIPBOARD_CANCEL_UNAVAILABLE); + for (const struct NetworkClipboardTransfer * item = clipboardWrites; + item; item = item->next) + if (events->requestCancel) + events->requestCancel(opaque, item->localRequest, + LG_CLIPBOARD_CANCEL_UNAVAILABLE); + for (const struct NetworkFileTransfer * item = fileReads; + item; item = item->next) + if (events->fileCancel) + events->fileCancel(opaque, item->request.dataset, + item->request.request, LG_CLIPBOARD_FILE_ERROR_DISCONNECTED); + for (const struct NetworkFileTransfer * item = fileWrites; + item; item = item->next) + if (events->fileCancel) + events->fileCancel(opaque, item->request.dataset, + item->request.request, LG_CLIPBOARD_FILE_ERROR_DISCONNECTED); + for (const struct NetworkFileAcquisition * item = acquisitions; + item; item = item->next) + if (events->fileCancel) + events->fileCancel(opaque, item->dataset, + item->localAcquisition, LG_CLIPBOARD_FILE_ERROR_DISCONNECTED); + if (release && events->release) + events->release(opaque); + } + LG_UNLOCK(clipboard->eventLock); +} + +static bool networkClipboardTimer(void * opaque) +{ + NetworkClipboard * clipboard = opaque; + LG_LOCK(clipboard->stateLock); + const bool destroying = clipboard->destroying; + LG_UNLOCK(clipboard->stateLock); + if (destroying) + return false; + + networkClipboardProcess(clipboard); + return true; +} + +bool networkClipboardCreate(NetworkClipboard ** result, + NetworkConnection * connection) +{ + if (!result || !connection) + return false; + + NetworkClipboard * clipboard = calloc(1, sizeof(*clipboard)); + if (!clipboard) + return false; + + LG_LOCK_INIT(clipboard->stateLock); + LG_LOCK_INIT(clipboard->eventLock); + LG_LOCK_INIT(clipboard->statusLock); + clipboard->connection = connection; + clipboard->statusGeneration = 1; + clipboard->ownerID = nanotime() ^ + ((uint64_t)(uintptr_t)clipboard << 23); + clipboard->localSerial = nanotime() ^ + ((uint64_t)(uintptr_t)clipboard << 11); + clipboard->wireSerial = clipboard->localSerial ^ + UINT64_C(0x4c47434c49504244); + clipboard->remoteSerial = clipboard->wireSerial ^ + UINT64_C(0x4e4554574f524b43); + clipboard->localSerial &= ~KVMFR_CLIPBOARD_TRANSFER_HELPER; + if (!clipboard->ownerID) + clipboard->ownerID = 1; + if (!clipboard->localSerial) + clipboard->localSerial = 1; + if (!clipboard->wireSerial) + clipboard->wireSerial = 1; + if (!clipboard->remoteSerial) + clipboard->remoteSerial = 1; + networkClipboardRemoteOfferResetNL(clipboard); + + if (!lgCreateTimer(NETWORK_CLIPBOARD_TIMER_MS, + networkClipboardTimer, clipboard, &clipboard->timer)) + { + LG_LOCK_FREE(clipboard->statusLock); + LG_LOCK_FREE(clipboard->eventLock); + LG_LOCK_FREE(clipboard->stateLock); + free(clipboard); + return false; + } + + *result = clipboard; + return true; +} + +void networkClipboardConnectionState(NetworkClipboard * clipboard, + bool connected) +{ + if (!clipboard) + return; + + uint16_t clipboardVersion = 0; + uint16_t fileVersion = 0; + const bool clipboardService = connected && + networkConnectionServiceAvailable(clipboard->connection, + LG_NET_SERVICE_CLIPBOARD, LG_NET_CAPABILITY_RELIABLE) && + networkConnectionServiceVersion(clipboard->connection, + LG_NET_SERVICE_CLIPBOARD, &clipboardVersion) && + clipboardVersion == LG_NET_CLIPBOARD_VERSION_CURRENT; + const bool fileService = clipboardService && + networkConnectionServiceAvailable(clipboard->connection, + LG_NET_SERVICE_FILE, LG_NET_CAPABILITY_RELIABLE) && + networkConnectionServiceVersion(clipboard->connection, + LG_NET_SERVICE_FILE, &fileVersion) && + fileVersion == LG_NET_FILE_VERSION_CURRENT; + const bool clipboardBulk = clipboardService && + networkConnectionServiceAvailable(clipboard->connection, + LG_NET_SERVICE_CLIPBOARD, LG_NET_CAPABILITY_BULK); + const bool fileBulk = fileService && + networkConnectionServiceAvailable(clipboard->connection, + LG_NET_SERVICE_FILE, LG_NET_CAPABILITY_BULK); + + LG_LOCK(clipboard->stateLock); + struct NetworkClipboardTransfer * clipboardReads = + clipboard->clipboardReads; + struct NetworkClipboardTransfer * clipboardWrites = + clipboard->clipboardWrites; + + struct NetworkFileTransfer * fileReads = clipboard->fileReads; + struct NetworkFileTransfer * fileWrites = clipboard->fileWrites; + + struct NetworkFileAcquisition * acquisitions = + clipboard->fileAcquisitions; + const bool release = clipboard->remoteOffer.published; + + clipboard->clipboardReads = NULL; + clipboard->clipboardWrites = NULL; + clipboard->fileReads = NULL; + clipboard->fileWrites = NULL; + clipboard->fileAcquisitions = NULL; + clipboard->connected = connected; + clipboard->clipboardService = clipboardService; + clipboard->fileService = fileService; + clipboard->clipboardBulk = clipboardBulk; + clipboard->fileBulk = fileBulk; + clipboard->clipboardVersion = clipboardService ? clipboardVersion : 0; + clipboard->fileVersion = fileService ? fileVersion : 0; + clipboard->claimPending = false; + clipboard->claimed = false; + clipboard->claimSerial = 0; + clipboard->nextClaimNs = 0; + clipboard->nextKeepaliveNs = 0; + networkClipboardClearSendNL(clipboard); + networkClipboardClearInboundNL(clipboard); + networkClipboardRemoteOfferResetNL(clipboard); + networkClipboardSetAvailableNL(clipboard, clipboardService); + if (clipboardService && clipboard->attached) + networkClipboardQueueClaimNL(clipboard); + LG_UNLOCK(clipboard->stateLock); + + networkClipboardDispatchDisconnected(clipboard, + clipboardReads, clipboardWrites, fileReads, fileWrites, + acquisitions, release); + networkClipboardTransfersFree(clipboardReads); + networkClipboardTransfersFree(clipboardWrites); + networkClipboardFileTransfersFree(fileReads); + networkClipboardFileTransfersFree(fileWrites); + networkClipboardAcquisitionsFree(acquisitions); + + networkClipboardProcess(clipboard); +} + +void networkClipboardConnect(NetworkClipboard * clipboard) +{ + networkClipboardConnectionState(clipboard, true); +} + +void networkClipboardDisconnect(NetworkClipboard * clipboard) +{ + networkClipboardConnectionState(clipboard, false); +} + +void networkClipboardDestroy(NetworkClipboard ** result) +{ + if (!result || !*result) + return; + + NetworkClipboard * clipboard = *result; + networkClipboardSetStatusListener(clipboard, NULL, NULL); + networkClipboardDetach(clipboard); + + LG_LOCK(clipboard->stateLock); + clipboard->destroying = true; + LG_UNLOCK(clipboard->stateLock); + lgTimerDestroy(clipboard->timer); + + networkClipboardConnectionState(clipboard, false); + LG_LOCK(clipboard->stateLock); + networkClipboardClearSendNL(clipboard); + networkClipboardClearInboundNL(clipboard); + LG_UNLOCK(clipboard->stateLock); + LG_LOCK_FREE(clipboard->statusLock); + LG_LOCK_FREE(clipboard->eventLock); + LG_LOCK_FREE(clipboard->stateLock); + free(clipboard); + *result = NULL; +} + +void networkClipboardProcess(NetworkClipboard * clipboard) +{ + if (!clipboard) + return; + + LG_LOCK(clipboard->stateLock); + if (clipboard->destroying || clipboard->processing) + { + LG_UNLOCK(clipboard->stateLock); + return; + } + clipboard->processing = true; + + bool progress = clipboard->sendProgress; + clipboard->sendProgress = false; + + const uint64_t now = nanotime(); + if (clipboard->connected && clipboard->clipboardService && + clipboard->attached) + { + if (clipboard->claimPending && now >= clipboard->nextClaimNs) + { + clipboard->claimPending = false; + clipboard->claimed = false; + } + if (!clipboard->claimPending && !clipboard->claimed && + now >= clipboard->nextClaimNs) + networkClipboardQueueClaimNL(clipboard); + else if (clipboard->claimed && now >= clipboard->nextKeepaliveNs) + { + if (networkClipboardQueueControlNL(clipboard, + LG_NET_CLIPBOARD_MESSAGE_KEEPALIVE, 0)) + clipboard->nextKeepaliveNs = networkClipboardDeadline( + NETWORK_CLIPBOARD_KEEPALIVE_MS); + } + } + progress |= networkClipboardFlushNL(clipboard); + LG_UNLOCK(clipboard->stateLock); + + networkClipboardProcessInbound(clipboard); + + LG_ClipboardRequest clipboardReady[NETWORK_CLIPBOARD_FILE_REQUESTS]; + uint64_t fileReady[NETWORK_CLIPBOARD_FILE_REQUESTS]; + unsigned clipboardReadyCount = 0; + unsigned fileReadyCount = 0; + + LG_LOCK(clipboard->stateLock); + progress |= clipboard->sendProgress; + clipboard->sendProgress = false; + progress |= networkClipboardFlushNL(clipboard); + if (progress) + { + for (struct NetworkClipboardTransfer * item = + clipboard->clipboardWrites; item && + clipboardReadyCount < NETWORK_CLIPBOARD_FILE_REQUESTS; + item = item->next) + { + if (!item->blocked) + continue; + item->blocked = false; + clipboardReady[clipboardReadyCount++] = item->localRequest; + } + for (struct NetworkFileTransfer * item = clipboard->fileWrites; + item && fileReadyCount < NETWORK_CLIPBOARD_FILE_REQUESTS; + item = item->next) + { + if (!item->blocked) + continue; + item->blocked = false; + fileReady[fileReadyCount++] = item->request.request; + } + } + LG_UNLOCK(clipboard->stateLock); + + for (unsigned index = 0; index < clipboardReadyCount; ++index) + networkClipboardDispatchDataReady( + clipboard, clipboardReady[index]); + for (unsigned index = 0; index < fileReadyCount; ++index) + networkClipboardDispatchFileReady(clipboard, fileReady[index]); + + networkClipboardNotifyStatus(clipboard); + + LG_LOCK(clipboard->stateLock); + clipboard->processing = false; + LG_UNLOCK(clipboard->stateLock); +} + +void networkClipboardSendReady(NetworkClipboard * clipboard, + LGNetService service, NetworkPacketDelivery delivery) +{ + if (!clipboard || (service != LG_NET_SERVICE_CLIPBOARD && + service != LG_NET_SERVICE_FILE) || + (delivery != NETWORK_PACKET_DELIVERY_RELIABLE && + delivery != NETWORK_PACKET_DELIVERY_BULK)) + return; + networkClipboardProcess(clipboard); +} + +bool networkClipboardPacket(NetworkClipboard * clipboard, + const LGNetPacketView * packet, NetworkPacketDelivery delivery) +{ + if (!clipboard || !packet || + (packet->envelope.service != LG_NET_SERVICE_CLIPBOARD && + packet->envelope.service != LG_NET_SERVICE_FILE) || + (delivery != NETWORK_PACKET_DELIVERY_RELIABLE && + delivery != NETWORK_PACKET_DELIVERY_BULK) || + (packet->payloadLength && !packet->payload) || + packet->payloadLength > LG_NET_MAX_PAYLOAD_LENGTH || + packet->payloadLength > + SIZE_MAX - sizeof(struct NetworkClipboardInbound)) + return false; + + struct NetworkClipboardInbound * inbound = + malloc(sizeof(*inbound) + packet->payloadLength); + if (!inbound) + return false; + inbound->next = NULL; + inbound->envelope = packet->envelope; + inbound->delivery = delivery; + inbound->payloadLength = packet->payloadLength; + if (packet->payloadLength) + memcpy(inbound->payload, packet->payload, packet->payloadLength); + + const size_t bytes = sizeof(*inbound) + inbound->payloadLength; + LG_LOCK(clipboard->stateLock); + const bool serviceAvailable = clipboard->connected && + ((packet->envelope.service == LG_NET_SERVICE_CLIPBOARD && + clipboard->clipboardService) || + (packet->envelope.service == LG_NET_SERVICE_FILE && + clipboard->fileService)); + const bool claimResult = packet->envelope.service == + LG_NET_SERVICE_CLIPBOARD && packet->envelope.messageType == + LG_NET_CLIPBOARD_MESSAGE_CLAIM_RESULT; + const bool stale = !claimResult && + packet->envelope.componentEpoch != clipboard->claimEpoch; + if (!serviceAvailable || stale) + { + LG_UNLOCK(clipboard->stateLock); + free(inbound); + return true; + } + if (clipboard->inboundCount >= NETWORK_CLIPBOARD_INBOUND_MAX || + bytes > NETWORK_CLIPBOARD_INBOUND_BYTES || + clipboard->inboundBytes > NETWORK_CLIPBOARD_INBOUND_BYTES - bytes) + { + LG_UNLOCK(clipboard->stateLock); + free(inbound); + return false; + } + + if (clipboard->inboundTail) + clipboard->inboundTail->next = inbound; + else + clipboard->inboundHead = inbound; + clipboard->inboundTail = inbound; + ++clipboard->inboundCount; + clipboard->inboundBytes += bytes; + LG_UNLOCK(clipboard->stateLock); + + networkClipboardProcess(clipboard); + return true; +} + +static const LG_ClipboardOps NETWORK_CLIPBOARD_OPS = +{ + .name = "Network", + .setStatusListener = networkClipboardSetStatusListener, + .attach = networkClipboardAttach, + .detach = networkClipboardDetach, + .release = networkClipboardRelease, + .notifyTypes = networkClipboardNotifyTypes, + .offerFiles = networkClipboardOfferFiles, + .data = networkClipboardData, + .dataBegin = networkClipboardDataBegin, + .dataChunk = networkClipboardDataChunk, + .dataEnd = networkClipboardDataEnd, + .dataCancel = networkClipboardDataCancel, + .dataReady = networkClipboardDataReady, + .request = networkClipboardRequest, + .fileAcquire = networkClipboardFileAcquire, + .fileAcquired = networkClipboardFileAcquired, + .fileRelease = networkClipboardFileRelease, + .fileRequest = networkClipboardFileRequest, + .fileDataBegin = networkClipboardFileDataBegin, + .fileDataChunk = networkClipboardFileDataChunk, + .fileDataEnd = networkClipboardFileDataEnd, + .fileCancel = networkClipboardFileCancel, +}; + +const LG_ClipboardOps * networkClipboardGetOps(void) +{ + return &NETWORK_CLIPBOARD_OPS; +} diff --git a/client/transports/Network/clipboard.h b/client/transports/Network/clipboard.h new file mode 100644 index 00000000..bf46507e --- /dev/null +++ b/client/transports/Network/clipboard.h @@ -0,0 +1,48 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#pragma once + +#include "connection.h" + +#include "interface/clipboard.h" + +#include + +#include + +typedef struct NetworkClipboard NetworkClipboard; + +bool networkClipboardCreate(NetworkClipboard ** clipboard, + NetworkConnection * connection); +void networkClipboardDestroy(NetworkClipboard ** clipboard); + +void networkClipboardConnectionState(NetworkClipboard * clipboard, + bool connected); +void networkClipboardConnect(NetworkClipboard * clipboard); +void networkClipboardDisconnect(NetworkClipboard * clipboard); +void networkClipboardProcess(NetworkClipboard * clipboard); +void networkClipboardSendReady(NetworkClipboard * clipboard, + LGNetService service, NetworkPacketDelivery delivery); + +bool networkClipboardPacket(NetworkClipboard * clipboard, + const LGNetPacketView * packet, NetworkPacketDelivery delivery); + +const LG_ClipboardOps * networkClipboardGetOps(void); diff --git a/client/transports/Network/connection.c b/client/transports/Network/connection.c index 7abe5636..57726445 100644 --- a/client/transports/Network/connection.c +++ b/client/transports/Network/connection.c @@ -77,6 +77,9 @@ #define NETWORK_SEND_DATAGRAM_COUNT_RESERVED 64U #define NETWORK_SEND_DATAGRAM_BYTES_RESERVED \ (UINT64_C(256) * 1024U) +#define NETWORK_SEND_BULK_COUNT_MAX 256U +#define NETWORK_SEND_BULK_BYTES_MAX \ + (UINT64_C(256) * 1024U * 1024U) #define NETWORK_SEND_RESUME_NUMERATOR 3U #define NETWORK_SEND_RESUME_DENOMINATOR 4U @@ -85,11 +88,12 @@ _Static_assert(NETWORK_AUTH_SHA256_SIZE == LG_NET_PASSWORD_SHA256_LENGTH, struct NetworkSend { - QUIC_BUFFER buffer; - LGNetService service; - uint32_t length; - bool datagram; - uint8_t data[]; + QUIC_BUFFER buffer; + LGNetService service; + uint32_t length; + NetworkPacketDelivery delivery; + bool accounted; + uint8_t data[]; }; struct NetworkSendLimits @@ -108,6 +112,27 @@ struct NetworkSendAccounting uint64_t serviceBlockedBytes[NETWORK_SEND_SERVICE_COUNT]; }; +struct NetworkBulkStream +{ + struct NetworkBulkStream * next; + NetworkConnection * connection; + HQUIC handle; + LGNetService service; + uint64_t streamID; + uint64_t sendSequence; + uint64_t receiveSequence; + bool outbound; + bool bound; + bool openingReturned; + bool shutdownComplete; + uint8_t bindingData[LG_NET_STREAM_BINDING_WIRE_SIZE]; + size_t bindingLength; + LGNetStreamParser receiveParser; + uint8_t * receiveData; + uint8_t * receivePayload; + size_t receivePayloadSize; +}; + struct NetworkConnection { const QUIC_API_TABLE * api; @@ -167,7 +192,14 @@ struct NetworkConnection uint16_t receivedServices; uint16_t reliableServices; uint16_t datagramServices; + uint16_t bulkServices; + uint16_t maxPeerBulkStreams; + uint16_t peerBulkStreams; uint16_t serviceVersions[LG_NET_SERVICE_CONTROL + 1U]; + uint16_t serviceMaxUnidirectional + [LG_NET_SERVICE_CONTROL + 1U]; + uint16_t servicePeerBulkStreams + [LG_NET_SERVICE_CONTROL + 1U]; uint32_t maxPayloadLength; uint32_t maxDatagramPayload; uint16_t maxDatagramSendLength; @@ -177,6 +209,11 @@ struct NetworkConnection struct NetworkSendAccounting reliableSend; struct NetworkSendAccounting datagramSend; + struct NetworkSendAccounting bulkSend; + + struct NetworkBulkStream * outboundBulk + [LG_NET_SERVICE_CONTROL + 1U]; + struct NetworkBulkStream * inboundBulk; uint8_t clientHelloNonce[LG_NET_HELLO_NONCE_LENGTH]; uint8_t serverHelloNonce[LG_NET_HELLO_NONCE_LENGTH]; @@ -193,6 +230,8 @@ static QUIC_STATUS QUIC_API networkConnectionCallback(HQUIC handle, void * opaque, QUIC_CONNECTION_EVENT * event); static QUIC_STATUS QUIC_API networkStreamCallback(HQUIC handle, void * opaque, QUIC_STREAM_EVENT * event); +static QUIC_STATUS QUIC_API networkBulkStreamCallback(HQUIC handle, + void * opaque, QUIC_STREAM_EVENT * event); static QUIC_STATUS QUIC_API networkRejectedStreamCallback(HQUIC handle, void * opaque, QUIC_STREAM_EVENT * event); @@ -232,6 +271,10 @@ static bool networkCapabilityOfferValid( capability->flags & LG_NET_CAPABILITY_SELECTED || capability->versionMin < version->minimum || capability->versionMax > version->maximum || + capability->maxBidirectionalStreams || + capability->maxUnidirectionalStreams > NETWORK_MAX_PEER_STREAMS || + (!!(capability->flags & LG_NET_CAPABILITY_BULK) != + !!capability->maxUnidirectionalStreams) || !(capability->flags & (LG_NET_CAPABILITY_RELIABLE | LG_NET_CAPABILITY_DATAGRAM | LG_NET_CAPABILITY_BULK))) return false; @@ -305,9 +348,9 @@ static void networkConnectionFail(NetworkConnection * this, } static struct NetworkSendLimits networkConnectionSendLimits( - LGNetService service, bool datagram) + LGNetService service, NetworkPacketDelivery delivery) { - if (datagram) + if (delivery == NETWORK_PACKET_DELIVERY_DATAGRAM) { switch (service) { @@ -341,6 +384,35 @@ static struct NetworkSendLimits networkConnectionSendLimits( } } + if (delivery == NETWORK_PACKET_DELIVERY_BULK) + { + switch (service) + { + case LG_NET_SERVICE_VIDEO: + return (struct NetworkSendLimits) + { + .count = 128U, + .bytes = UINT64_C(192) * 1024U * 1024U, + }; + + case LG_NET_SERVICE_CLIPBOARD: + case LG_NET_SERVICE_FILE: + case LG_NET_SERVICE_USB: + return (struct NetworkSendLimits) + { + .count = 128U, + .bytes = UINT64_C(128) * 1024U * 1024U, + }; + + default: + return (struct NetworkSendLimits) + { + .count = 64U, + .bytes = UINT64_C(64) * 1024U * 1024U, + }; + } + } + switch (service) { case LG_NET_SERVICE_CURSOR: @@ -402,32 +474,73 @@ static void networkConnectionResetSendAccountingNL( { memset(&this->reliableSend, 0, sizeof(this->reliableSend)); memset(&this->datagramSend, 0, sizeof(this->datagramSend)); + memset(&this->bulkSend, 0, sizeof(this->bulkSend)); +} + +static struct NetworkSendAccounting * networkConnectionSendAccountingNL( + NetworkConnection * this, NetworkPacketDelivery delivery) +{ + switch (delivery) + { + case NETWORK_PACKET_DELIVERY_RELIABLE: + return &this->reliableSend; + + case NETWORK_PACKET_DELIVERY_DATAGRAM: + return &this->datagramSend; + + case NETWORK_PACKET_DELIVERY_BULK: + return &this->bulkSend; + } + + return NULL; } static LG_TransportStatus networkConnectionReserveSendNL( - NetworkConnection * this, LGNetService service, bool datagram, - uint64_t packetSize) + NetworkConnection * this, LGNetService service, + NetworkPacketDelivery delivery, uint64_t packetSize) { if (service >= NETWORK_SEND_SERVICE_COUNT) return LG_TRANSPORT_ERROR; const struct NetworkSendLimits limits = - networkConnectionSendLimits(service, datagram); - uint32_t totalCountLimit = datagram ? - NETWORK_SEND_DATAGRAM_COUNT_MAX : NETWORK_SEND_RELIABLE_COUNT_MAX; - uint64_t totalByteLimit = datagram ? - NETWORK_SEND_DATAGRAM_BYTES_MAX : NETWORK_SEND_RELIABLE_BYTES_MAX; - if (!networkConnectionSendPriority(service)) + networkConnectionSendLimits(service, delivery); + uint32_t totalCountLimit; + uint64_t totalByteLimit; + switch (delivery) { - totalCountLimit -= datagram ? + case NETWORK_PACKET_DELIVERY_RELIABLE: + totalCountLimit = NETWORK_SEND_RELIABLE_COUNT_MAX; + totalByteLimit = NETWORK_SEND_RELIABLE_BYTES_MAX; + break; + + case NETWORK_PACKET_DELIVERY_DATAGRAM: + totalCountLimit = NETWORK_SEND_DATAGRAM_COUNT_MAX; + totalByteLimit = NETWORK_SEND_DATAGRAM_BYTES_MAX; + break; + + case NETWORK_PACKET_DELIVERY_BULK: + totalCountLimit = NETWORK_SEND_BULK_COUNT_MAX; + totalByteLimit = NETWORK_SEND_BULK_BYTES_MAX; + break; + + default: + return LG_TRANSPORT_ERROR; + } + + if (delivery != NETWORK_PACKET_DELIVERY_BULK && + !networkConnectionSendPriority(service)) + { + totalCountLimit -= delivery == NETWORK_PACKET_DELIVERY_DATAGRAM ? NETWORK_SEND_DATAGRAM_COUNT_RESERVED : NETWORK_SEND_RELIABLE_COUNT_RESERVED; - totalByteLimit -= datagram ? + totalByteLimit -= delivery == NETWORK_PACKET_DELIVERY_DATAGRAM ? NETWORK_SEND_DATAGRAM_BYTES_RESERVED : NETWORK_SEND_RELIABLE_BYTES_RESERVED; } - struct NetworkSendAccounting * accounting = datagram ? - &this->datagramSend : &this->reliableSend; + struct NetworkSendAccounting * accounting = + networkConnectionSendAccountingNL(this, delivery); + if (!accounting) + return LG_TRANSPORT_ERROR; if (packetSize > limits.bytes || packetSize > totalByteLimit) return LG_TRANSPORT_ERROR; @@ -451,26 +564,64 @@ static LG_TransportStatus networkConnectionReserveSendNL( return LG_TRANSPORT_OK; } +static void networkConnectionMarkSendBlockedNL(NetworkConnection * this, + LGNetService service, NetworkPacketDelivery delivery, + uint64_t packetSize) +{ + struct NetworkSendAccounting * accounting = + networkConnectionSendAccountingNL(this, delivery); + if (!accounting || service >= NETWORK_SEND_SERVICE_COUNT) + return; + + accounting->serviceBlocked[service] = true; + accounting->serviceBlockedBytes[service] = + accounting->serviceBlockedBytes[service] > packetSize ? + accounting->serviceBlockedBytes[service] : packetSize; +} + static bool networkConnectionSendCanResumeNL( - const NetworkConnection * this, LGNetService service, bool datagram) + NetworkConnection * this, LGNetService service, + NetworkPacketDelivery delivery) { const struct NetworkSendLimits limits = - networkConnectionSendLimits(service, datagram); - uint32_t totalCountLimit = datagram ? - NETWORK_SEND_DATAGRAM_COUNT_MAX : NETWORK_SEND_RELIABLE_COUNT_MAX; - uint64_t totalByteLimit = datagram ? - NETWORK_SEND_DATAGRAM_BYTES_MAX : NETWORK_SEND_RELIABLE_BYTES_MAX; - if (!networkConnectionSendPriority(service)) + networkConnectionSendLimits(service, delivery); + uint32_t totalCountLimit; + uint64_t totalByteLimit; + switch (delivery) { - totalCountLimit -= datagram ? + case NETWORK_PACKET_DELIVERY_RELIABLE: + totalCountLimit = NETWORK_SEND_RELIABLE_COUNT_MAX; + totalByteLimit = NETWORK_SEND_RELIABLE_BYTES_MAX; + break; + + case NETWORK_PACKET_DELIVERY_DATAGRAM: + totalCountLimit = NETWORK_SEND_DATAGRAM_COUNT_MAX; + totalByteLimit = NETWORK_SEND_DATAGRAM_BYTES_MAX; + break; + + case NETWORK_PACKET_DELIVERY_BULK: + totalCountLimit = NETWORK_SEND_BULK_COUNT_MAX; + totalByteLimit = NETWORK_SEND_BULK_BYTES_MAX; + break; + + default: + return false; + } + + if (delivery != NETWORK_PACKET_DELIVERY_BULK && + !networkConnectionSendPriority(service)) + { + totalCountLimit -= delivery == NETWORK_PACKET_DELIVERY_DATAGRAM ? NETWORK_SEND_DATAGRAM_COUNT_RESERVED : NETWORK_SEND_RELIABLE_COUNT_RESERVED; - totalByteLimit -= datagram ? + totalByteLimit -= delivery == NETWORK_PACKET_DELIVERY_DATAGRAM ? NETWORK_SEND_DATAGRAM_BYTES_RESERVED : NETWORK_SEND_RELIABLE_BYTES_RESERVED; } - const struct NetworkSendAccounting * accounting = datagram ? - &this->datagramSend : &this->reliableSend; + const struct NetworkSendAccounting * accounting = + networkConnectionSendAccountingNL(this, delivery); + if (!accounting) + return false; const uint32_t totalCount = accounting->count; const uint64_t totalBytes = accounting->bytes; const uint32_t serviceCount = @@ -482,9 +633,14 @@ static bool networkConnectionSendCanResumeNL( if (atomic_load_explicit(&this->stopping, memory_order_acquire) || !atomic_load_explicit(&this->sessionValid, memory_order_acquire) || - !this->controlStream || - (datagram && (!this->datagramSendEnabled || - blockedBytes > this->maxDatagramSendLength))) + (delivery == NETWORK_PACKET_DELIVERY_RELIABLE && + !this->controlStream) || + (delivery == NETWORK_PACKET_DELIVERY_DATAGRAM && + (!this->datagramSendEnabled || + blockedBytes > this->maxDatagramSendLength)) || + (delivery == NETWORK_PACKET_DELIVERY_BULK && + (!this->outboundBulk[service] || + !this->outboundBulk[service]->bound))) return false; const uint32_t totalResumeCount = @@ -511,16 +667,19 @@ static bool networkConnectionSendCanResumeNL( } static uint16_t networkConnectionReadyServicesNL( - NetworkConnection * this, bool datagram) + NetworkConnection * this, NetworkPacketDelivery delivery) { uint16_t ready = 0; - struct NetworkSendAccounting * accounting = datagram ? - &this->datagramSend : &this->reliableSend; + struct NetworkSendAccounting * accounting = + networkConnectionSendAccountingNL(this, delivery); + if (!accounting) + return 0; + for (LGNetService service = LG_NET_SERVICE_CORE; service < NETWORK_SEND_SERVICE_COUNT; ++service) { if (!accounting->serviceBlocked[service] || - !networkConnectionSendCanResumeNL(this, service, datagram)) + !networkConnectionSendCanResumeNL(this, service, delivery)) continue; accounting->serviceBlocked[service] = false; @@ -533,14 +692,22 @@ static uint16_t networkConnectionReadyServicesNL( static uint16_t networkConnectionReleaseSendNL( NetworkConnection * this, const struct NetworkSend * send) { + if (!send->accounted) + return 0; + if (send->service >= NETWORK_SEND_SERVICE_COUNT) { DEBUG_ERROR("Network send accounting service out of range"); return 0; } - struct NetworkSendAccounting * accounting = send->datagram ? - &this->datagramSend : &this->reliableSend; + struct NetworkSendAccounting * accounting = + networkConnectionSendAccountingNL(this, send->delivery); + if (!accounting) + { + DEBUG_ERROR("Network send accounting delivery out of range"); + return 0; + } if (!accounting->count || !accounting->serviceCount[send->service] || accounting->bytes < send->length || accounting->serviceBytes[send->service] < send->length) @@ -554,11 +721,11 @@ static uint16_t networkConnectionReleaseSendNL( --accounting->serviceCount[send->service]; accounting->bytes -= send->length; accounting->serviceBytes[send->service] -= send->length; - return networkConnectionReadyServicesNL(this, send->datagram); + return networkConnectionReadyServicesNL(this, send->delivery); } static void networkConnectionNotifySendReady(NetworkConnection * this, - uint16_t reliable, uint16_t datagram) + uint16_t reliable, uint16_t datagram, uint16_t bulk) { if (!this->sendReady) return; @@ -568,9 +735,14 @@ static void networkConnectionNotifySendReady(NetworkConnection * this, { const uint16_t serviceMask = UINT16_C(1) << service; if (reliable & serviceMask) - this->sendReady(this->opaque, service, false); + this->sendReady(this->opaque, service, + NETWORK_PACKET_DELIVERY_RELIABLE); if (datagram & serviceMask) - this->sendReady(this->opaque, service, true); + this->sendReady(this->opaque, service, + NETWORK_PACKET_DELIVERY_DATAGRAM); + if (bulk & serviceMask) + this->sendReady(this->opaque, service, + NETWORK_PACKET_DELIVERY_BULK); } } @@ -633,7 +805,8 @@ static LG_TransportStatus networkConnectionSendPacket( } const LG_TransportStatus reserveStatus = - networkConnectionReserveSendNL(this, service, false, packetSize); + networkConnectionReserveSendNL(this, service, + NETWORK_PACKET_DELIVERY_RELIABLE, packetSize); if (reserveStatus != LG_TRANSPORT_OK) { LG_UNLOCK(this->lock); @@ -645,13 +818,14 @@ static LG_TransportStatus networkConnectionSendPacket( { const struct NetworkSend rollback = { - .service = service, - .length = (uint32_t)packetSize, - .datagram = false, + .service = service, + .length = (uint32_t)packetSize, + .delivery = NETWORK_PACKET_DELIVERY_RELIABLE, + .accounted = true, }; const uint16_t ready = networkConnectionReleaseSendNL(this, &rollback); LG_UNLOCK(this->lock); - networkConnectionNotifySendReady(this, ready, 0); + networkConnectionNotifySendReady(this, ready, 0, 0); return LG_TRANSPORT_ERROR; } @@ -659,7 +833,8 @@ static LG_TransportStatus networkConnectionSendPacket( parameters.sequence = ++this->sendSequence; send->service = service; send->length = (uint32_t)packetSize; - send->datagram = false; + send->delivery = NETWORK_PACKET_DELIVERY_RELIABLE; + send->accounted = true; size_t wireSize; if (!lgNetPacketConstruct(send->data, packetSize, &wireSize, ¶meters, payload, payloadLength) || wireSize != packetSize) @@ -668,7 +843,7 @@ static LG_TransportStatus networkConnectionSendPacket( const uint16_t ready = networkConnectionReleaseSendNL(this, send); LG_UNLOCK(this->lock); free(send); - networkConnectionNotifySendReady(this, ready, 0); + networkConnectionNotifySendReady(this, ready, 0, 0); return LG_TRANSPORT_ERROR; } @@ -688,7 +863,7 @@ static LG_TransportStatus networkConnectionSendPacket( if (QUIC_FAILED(status)) { free(send); - networkConnectionNotifySendReady(this, ready, 0); + networkConnectionNotifySendReady(this, ready, 0, 0); return LG_TRANSPORT_ERROR; } return LG_TRANSPORT_OK; @@ -822,21 +997,33 @@ static bool networkConnectionHandleCapability(NetworkConnection * this, offered.maxBidirectionalStreams || capability.maxUnidirectionalStreams > offered.maxUnidirectionalStreams || + capability.maxBidirectionalStreams || + (!!(selectedFlags & LG_NET_CAPABILITY_BULK) != + !!capability.maxUnidirectionalStreams) || + capability.maxUnidirectionalStreams > + NETWORK_MAX_PEER_STREAMS - this->maxPeerBulkStreams || (this->receivedServices & serviceMask)) { LG_UNLOCK(this->lock); return false; } + const LGNetService service = capability.service; this->receivedServices |= serviceMask; if (capability.flags & LG_NET_CAPABILITY_RELIABLE) - this->reliableServices |= UINT16_C(1) << capability.service; + this->reliableServices |= UINT16_C(1) << service; if (capability.flags & LG_NET_CAPABILITY_DATAGRAM) - this->datagramServices |= UINT16_C(1) << capability.service; - this->serviceVersions[capability.service] = capability.versionMin; - this->serviceMaxPayload[capability.service] = - capability.maxPayloadLength; - this->serviceMaxDatagram[capability.service] = + this->datagramServices |= UINT16_C(1) << service; + if (capability.flags & LG_NET_CAPABILITY_BULK) + { + this->bulkServices |= UINT16_C(1) << service; + this->maxPeerBulkStreams += capability.maxUnidirectionalStreams; + } + this->serviceVersions[service] = capability.versionMin; + this->serviceMaxUnidirectional[service] = + capability.maxUnidirectionalStreams; + this->serviceMaxPayload[service] = capability.maxPayloadLength; + this->serviceMaxDatagram[service] = capability.maxDatagramPayload; ++this->receivedCapabilities; LG_UNLOCK(this->lock); @@ -1262,22 +1449,42 @@ static bool networkConnectionHandleCore(NetworkConnection * this, } static bool networkConnectionHandlePacket(NetworkConnection * this, - const LGNetPacketView * packet, bool datagram) + const LGNetPacketView * packet, NetworkPacketDelivery delivery) { if (packet->envelope.service == LG_NET_SERVICE_CORE) - return networkConnectionHandleCore(this, packet); + return delivery == NETWORK_PACKET_DELIVERY_RELIABLE && + networkConnectionHandleCore(this, packet); if (!networkConnectionServiceValid(packet->envelope.service) || !atomic_load_explicit(&this->sessionValid, memory_order_acquire)) return false; LG_LOCK(this->lock); + const uint16_t serviceMask = + UINT16_C(1) << packet->envelope.service; + uint16_t deliveryServices; + switch (delivery) + { + case NETWORK_PACKET_DELIVERY_RELIABLE: + deliveryServices = this->reliableServices; + break; + + case NETWORK_PACKET_DELIVERY_DATAGRAM: + deliveryServices = this->datagramServices; + break; + + case NETWORK_PACKET_DELIVERY_BULK: + deliveryServices = this->bulkServices; + break; + + default: + LG_UNLOCK(this->lock); + return false; + } const bool sessionValid = packet->envelope.sessionEpoch == this->sessionEpoch && - (this->receivedServices & - (UINT16_C(1) << packet->envelope.service)) && - (this->reliableServices & - (UINT16_C(1) << packet->envelope.service)) && + (this->receivedServices & serviceMask) && + (deliveryServices & serviceMask) && packet->envelope.serviceVersion == this->serviceVersions[packet->envelope.service] && packet->payloadLength <= @@ -1287,7 +1494,7 @@ static bool networkConnectionHandlePacket(NetworkConnection * this, return false; if (this->packet) - this->packet(this->opaque, packet, datagram); + this->packet(this->opaque, packet, delivery); return true; } @@ -1394,7 +1601,8 @@ static bool networkConnectionDrainReceive(NetworkConnection * this) this->receiveSequence = packet.envelope.sequence; LG_UNLOCK(this->lock); if (!sequenceValid || !networkConnectionValidateControl(this, &packet) || - !networkConnectionHandlePacket(this, &packet, false) || + !networkConnectionHandlePacket(this, &packet, + NETWORK_PACKET_DELIVERY_RELIABLE) || !lgNetStreamParserConsume(&this->receiveParser)) return false; } @@ -1429,6 +1637,382 @@ static bool networkConnectionReceive(NetworkConnection * this, return networkConnectionDrainReceive(this); } +static void networkBulkStreamFree(struct NetworkBulkStream * stream) +{ + if (!stream) + return; + + free(stream->receivePayload); + free(stream->receiveData); + free(stream); +} + +static void networkBulkStreamDetachNL(struct NetworkBulkStream * stream) +{ + NetworkConnection * this = stream->connection; + if (stream->outbound) + { + if (stream->service < NETWORK_SEND_SERVICE_COUNT && + this->outboundBulk[stream->service] == stream) + this->outboundBulk[stream->service] = NULL; + return; + } + + struct NetworkBulkStream ** link = &this->inboundBulk; + while (*link && *link != stream) + link = &(*link)->next; + if (*link != stream) + return; + *link = stream->next; + + if (this->peerBulkStreams) + --this->peerBulkStreams; + if (stream->bound && stream->service < NETWORK_SEND_SERVICE_COUNT && + this->servicePeerBulkStreams[stream->service]) + --this->servicePeerBulkStreams[stream->service]; +} + +static bool networkBulkStreamActivateReceive( + struct NetworkBulkStream * stream) +{ + LGNetStreamBinding binding; + if (lgNetStreamBindingDecode(&binding, stream->bindingData, + sizeof(stream->bindingData)) != LG_NET_PARSE_OK || + binding.originRole != LG_NET_ROLE_SERVER || + binding.flags != (LG_NET_STREAM_BINDING_UNIDIRECTIONAL | + LG_NET_STREAM_BINDING_BULK) || + binding.streamID != stream->streamID || + !networkConnectionServiceValid(binding.service)) + return false; + + NetworkConnection * this = stream->connection; + uint32_t maxPayloadLength; + LG_LOCK(this->lock); + const uint16_t serviceMask = UINT16_C(1) << binding.service; + const bool valid = + !atomic_load_explicit(&this->stopping, memory_order_acquire) && + atomic_load_explicit(&this->sessionValid, memory_order_acquire) && + binding.sessionEpoch == this->sessionEpoch && + (this->receivedServices & serviceMask) && + (this->bulkServices & serviceMask) && + this->serviceMaxUnidirectional[binding.service] && + this->servicePeerBulkStreams[binding.service] < + this->serviceMaxUnidirectional[binding.service]; + maxPayloadLength = valid ? + this->serviceMaxPayload[binding.service] : 0; + LG_UNLOCK(this->lock); + if (!valid || !maxPayloadLength) + return false; + + const size_t receiveSize = + LG_NET_ENVELOPE_WIRE_SIZE + (size_t)maxPayloadLength; + uint8_t * receiveData = malloc(receiveSize); + if (!receiveData) + return false; + + stream->receiveData = receiveData; + if (!lgNetStreamParserInit(&stream->receiveParser, receiveData, + receiveSize, maxPayloadLength)) + { + free(stream->receiveData); + stream->receiveData = NULL; + return false; + } + + LG_LOCK(this->lock); + const bool stateValid = + !atomic_load_explicit(&this->stopping, memory_order_acquire) && + atomic_load_explicit(&this->sessionValid, memory_order_acquire) && + binding.sessionEpoch == this->sessionEpoch && + (this->receivedServices & serviceMask) && + (this->bulkServices & serviceMask) && + this->serviceMaxPayload[binding.service] == maxPayloadLength && + this->serviceMaxUnidirectional[binding.service] && + this->servicePeerBulkStreams[binding.service] < + this->serviceMaxUnidirectional[binding.service]; + if (stateValid) + { + stream->service = binding.service; + stream->bound = true; + ++this->servicePeerBulkStreams[binding.service]; + } + LG_UNLOCK(this->lock); + if (!stateValid) + { + free(stream->receiveData); + stream->receiveData = NULL; + return false; + } + + return true; +} + +static bool networkConnectionValidateBulk( + const struct NetworkBulkStream * stream, + const LGNetPacketView * packet) +{ + return stream->bound && + packet->envelope.service == stream->service && + packet->envelope.headerSize == LG_NET_ENVELOPE_WIRE_SIZE && + packet->envelope.messageVersion == LG_NET_MESSAGE_VERSION_CURRENT && + (packet->envelope.flags & LG_NET_ENVELOPE_FINAL) && + !(packet->envelope.flags & (LG_NET_ENVELOPE_RELIABLE | + LG_NET_ENVELOPE_DATAGRAM | LG_NET_ENVELOPE_EARLY_DATA)) && + lgNetEnvelopeRouteValid(&packet->envelope, LG_NET_ROLE_SERVER, + LG_NET_ROUTE_DELIVERY_BULK); +} + +static bool networkBulkStreamDrainReceive( + struct NetworkBulkStream * stream) +{ + NetworkConnection * this = stream->connection; + for (;;) + { + LGNetStreamPacket streamPacket; + const LGNetStreamResult result = lgNetStreamParserNext( + &stream->receiveParser, &streamPacket, NULL); + if (result == LG_NET_STREAM_NEED_DATA) + return true; + if (result != LG_NET_STREAM_PACKET) + return false; + + LGNetPacketView packet = + { + .envelope = streamPacket.envelope, + .payload = streamPacket.payload[0].data, + .payloadLength = streamPacket.payloadLength, + .wireSize = streamPacket.wireSize, + }; + if (streamPacket.payload[1].size) + { + if (stream->receivePayloadSize < streamPacket.payloadLength) + { + uint8_t * receivePayload = realloc( + stream->receivePayload, streamPacket.payloadLength); + if (!receivePayload) + return false; + stream->receivePayload = receivePayload; + stream->receivePayloadSize = streamPacket.payloadLength; + } + memcpy(stream->receivePayload, streamPacket.payload[0].data, + streamPacket.payload[0].size); + memcpy(stream->receivePayload + streamPacket.payload[0].size, + streamPacket.payload[1].data, streamPacket.payload[1].size); + packet.payload = stream->receivePayload; + } + + LG_LOCK(this->lock); + const bool sequenceValid = stream->bound && + stream->receiveSequence != UINT64_MAX && + packet.envelope.sequence == stream->receiveSequence + 1U; + if (sequenceValid) + stream->receiveSequence = packet.envelope.sequence; + LG_UNLOCK(this->lock); + if (!sequenceValid || + !networkConnectionValidateBulk(stream, &packet) || + !networkConnectionHandlePacket(this, &packet, + NETWORK_PACKET_DELIVERY_BULK) || + !lgNetStreamParserConsume(&stream->receiveParser)) + return false; + + if (!lgNetStreamParserBuffered(&stream->receiveParser)) + lgNetStreamParserReset(&stream->receiveParser); + } +} + +static bool networkBulkStreamReceive(struct NetworkBulkStream * stream, + const QUIC_BUFFER * buffers, uint32_t count) +{ + if (count && !buffers) + return false; + + for (uint32_t index = 0; index < count; ++index) + { + if (buffers[index].Length && !buffers[index].Buffer) + return false; + + size_t offset = 0; + while (offset < buffers[index].Length) + { + if (!stream->bound) + { + const size_t remaining = buffers[index].Length - offset; + size_t bindingLength = sizeof(stream->bindingData) - + stream->bindingLength; + if (bindingLength > remaining) + bindingLength = remaining; + memcpy(stream->bindingData + stream->bindingLength, + buffers[index].Buffer + offset, bindingLength); + stream->bindingLength += bindingLength; + offset += bindingLength; + if (stream->bindingLength == sizeof(stream->bindingData) && + !networkBulkStreamActivateReceive(stream)) + return false; + continue; + } + + size_t accepted; + if (!lgNetStreamParserAppend(&stream->receiveParser, + buffers[index].Buffer + offset, + buffers[index].Length - offset, &accepted)) + return false; + offset += accepted; + if (!networkBulkStreamDrainReceive(stream) || + (!accepted && + !lgNetStreamParserWritable(&stream->receiveParser))) + return false; + } + } + + return !stream->bound || networkBulkStreamDrainReceive(stream); +} + +static LG_TransportStatus networkConnectionOpenBulkStream( + NetworkConnection * this, LGNetService service, uint64_t packetSize) +{ + struct NetworkBulkStream * stream = calloc(1, sizeof(*stream)); + if (!stream) + return LG_TRANSPORT_ERROR; + stream->connection = this; + stream->service = service; + stream->outbound = true; + + LG_LOCK(this->lock); + const uint16_t serviceMask = UINT16_C(1) << service; + HQUIC connection = this->connection; + if (!connection || + atomic_load_explicit(&this->stopping, memory_order_acquire) || + !atomic_load_explicit(&this->sessionValid, memory_order_acquire)) + { + LG_UNLOCK(this->lock); + networkBulkStreamFree(stream); + return LG_TRANSPORT_DISCONNECTED; + } + if (!(this->receivedServices & serviceMask) || + !(this->bulkServices & serviceMask) || + !this->serviceMaxUnidirectional[service]) + { + LG_UNLOCK(this->lock); + networkBulkStreamFree(stream); + return LG_TRANSPORT_UNAVAILABLE; + } + if (this->outboundBulk[service]) + { + this->outboundBulk[service]->openingReturned = true; + networkConnectionMarkSendBlockedNL(this, service, + NETWORK_PACKET_DELIVERY_BULK, packetSize); + LG_UNLOCK(this->lock); + networkBulkStreamFree(stream); + return LG_TRANSPORT_UNAVAILABLE; + } + + this->outboundBulk[service] = stream; + networkConnectionMarkSendBlockedNL(this, service, + NETWORK_PACKET_DELIVERY_BULK, packetSize); + LG_UNLOCK(this->lock); + + HQUIC handle; + QUIC_STATUS status = this->api->StreamOpen(connection, + QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL, + networkBulkStreamCallback, stream, &handle); + if (QUIC_FAILED(status)) + { + LG_LOCK(this->lock); + networkBulkStreamDetachNL(stream); + this->bulkSend.serviceBlocked[service] = false; + this->bulkSend.serviceBlockedBytes[service] = 0; + LG_UNLOCK(this->lock); + networkBulkStreamFree(stream); + return LG_TRANSPORT_ERROR; + } + stream->handle = handle; + + status = this->api->StreamStart(handle, + QUIC_STREAM_START_FLAG_IMMEDIATE); + if (QUIC_FAILED(status)) + { + LG_LOCK(this->lock); + networkBulkStreamDetachNL(stream); + this->bulkSend.serviceBlocked[service] = false; + this->bulkSend.serviceBlockedBytes[service] = 0; + LG_UNLOCK(this->lock); + this->api->StreamClose(handle); + networkBulkStreamFree(stream); + return LG_TRANSPORT_ERROR; + } + + uint16_t ready = 0; + LG_LOCK(this->lock); + stream->openingReturned = true; + if (stream->bound && !stream->shutdownComplete) + ready = networkConnectionReadyServicesNL( + this, NETWORK_PACKET_DELIVERY_BULK); + const bool bound = stream->bound && !stream->shutdownComplete; + const bool shutdownComplete = stream->shutdownComplete; + LG_UNLOCK(this->lock); + if (shutdownComplete) + networkBulkStreamFree(stream); + networkConnectionNotifySendReady(this, 0, 0, ready); + return shutdownComplete ? LG_TRANSPORT_DISCONNECTED : + bound ? LG_TRANSPORT_OK : LG_TRANSPORT_UNAVAILABLE; +} + +static void networkConnectionProtocolFailure(NetworkConnection * this) +{ + networkConnectionFail(this, LG_TRANSPORT_ERROR); + + LG_LOCK(this->lock); + HQUIC connection = this->connection; + LG_UNLOCK(this->lock); + if (connection) + this->api->ConnectionShutdown(connection, + QUIC_CONNECTION_SHUTDOWN_FLAG_SILENT, 0); +} + +static bool networkConnectionAcceptBulkStream(NetworkConnection * this, + HQUIC handle, QUIC_STREAM_OPEN_FLAGS flags) +{ + if (flags != QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL) + return false; + + QUIC_UINT62 streamID; + uint32_t streamIDSize = sizeof(streamID); + if (QUIC_FAILED(this->api->GetParam(handle, QUIC_PARAM_STREAM_ID, + &streamIDSize, &streamID)) || streamIDSize != sizeof(streamID) || + (streamID & 3U) != 3U) + return false; + + struct NetworkBulkStream * stream = calloc(1, sizeof(*stream)); + if (!stream) + return false; + stream->connection = this; + stream->handle = handle; + stream->streamID = streamID; + + LG_LOCK(this->lock); + const bool accepted = + !atomic_load_explicit(&this->stopping, memory_order_acquire) && + atomic_load_explicit(&this->sessionValid, memory_order_acquire) && + this->maxPeerBulkStreams && + this->peerBulkStreams < this->maxPeerBulkStreams; + if (accepted) + { + stream->next = this->inboundBulk; + this->inboundBulk = stream; + ++this->peerBulkStreams; + } + LG_UNLOCK(this->lock); + if (!accepted) + { + networkBulkStreamFree(stream); + return false; + } + + this->api->SetCallbackHandler(handle, + (void *)networkBulkStreamCallback, stream); + return true; +} + static void networkConnectionTrustDecision(void * opaque, bool trusted) { NetworkConnection * this = opaque; @@ -1593,11 +2177,17 @@ static QUIC_STATUS QUIC_API networkConnectionCallback(HQUIC handle, return QUIC_STATUS_SUCCESS; case QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED: - this->api->SetCallbackHandler(event->PEER_STREAM_STARTED.Stream, - (void *)networkRejectedStreamCallback, this); - this->api->StreamShutdown(event->PEER_STREAM_STARTED.Stream, - QUIC_STREAM_SHUTDOWN_FLAG_ABORT | - QUIC_STREAM_SHUTDOWN_FLAG_IMMEDIATE, 0); + if (!networkConnectionAcceptBulkStream(this, + event->PEER_STREAM_STARTED.Stream, + event->PEER_STREAM_STARTED.Flags)) + { + this->api->SetCallbackHandler(event->PEER_STREAM_STARTED.Stream, + (void *)networkRejectedStreamCallback, this); + this->api->StreamShutdown(event->PEER_STREAM_STARTED.Stream, + QUIC_STREAM_SHUTDOWN_FLAG_ABORT | + QUIC_STREAM_SHUTDOWN_FLAG_IMMEDIATE, 0); + networkConnectionProtocolFailure(this); + } return QUIC_STATUS_SUCCESS; case QUIC_CONNECTION_EVENT_DATAGRAM_RECEIVED: @@ -1605,7 +2195,8 @@ static QUIC_STATUS QUIC_API networkConnectionCallback(HQUIC handle, LGNetPacketView packet; if (networkConnectionDecodeDatagram( this, event->DATAGRAM_RECEIVED.Buffer, &packet) && this->packet) - this->packet(this->opaque, &packet, true); + this->packet(this->opaque, &packet, + NETWORK_PACKET_DELIVERY_DATAGRAM); return QUIC_STATUS_SUCCESS; } @@ -1618,9 +2209,10 @@ static QUIC_STATUS QUIC_API networkConnectionCallback(HQUIC handle, this->maxDatagramSendLength = event->DATAGRAM_STATE_CHANGED.MaxSendLength; ready = - networkConnectionReadyServicesNL(this, true); + networkConnectionReadyServicesNL( + this, NETWORK_PACKET_DELIVERY_DATAGRAM); LG_UNLOCK(this->lock); - networkConnectionNotifySendReady(this, 0, ready); + networkConnectionNotifySendReady(this, 0, ready, 0); return QUIC_STATUS_SUCCESS; } @@ -1635,7 +2227,7 @@ static QUIC_STATUS QUIC_API networkConnectionCallback(HQUIC handle, networkConnectionReleaseSendNL(this, send); LG_UNLOCK(this->lock); free(send); - networkConnectionNotifySendReady(this, 0, ready); + networkConnectionNotifySendReady(this, 0, ready, 0); } return QUIC_STATUS_SUCCESS; @@ -1681,7 +2273,7 @@ static QUIC_STATUS QUIC_API networkStreamCallback(HQUIC handle, const uint16_t ready = networkConnectionReleaseSendNL(this, send); LG_UNLOCK(this->lock); free(send); - networkConnectionNotifySendReady(this, ready, 0); + networkConnectionNotifySendReady(this, ready, 0, 0); return QUIC_STATUS_SUCCESS; } @@ -1705,6 +2297,157 @@ static QUIC_STATUS QUIC_API networkStreamCallback(HQUIC handle, } } +static QUIC_STATUS QUIC_API networkBulkStreamCallback(HQUIC handle, + void * opaque, QUIC_STREAM_EVENT * event) +{ + struct NetworkBulkStream * stream = opaque; + NetworkConnection * this = stream->connection; + + switch (event->Type) + { + case QUIC_STREAM_EVENT_START_COMPLETE: + { + if (QUIC_FAILED(event->START_COMPLETE.Status)) + { + LG_LOCK(this->lock); + networkBulkStreamDetachNL(stream); + const bool deferFree = + stream->outbound && !stream->openingReturned; + stream->shutdownComplete = true; + LG_UNLOCK(this->lock); + this->api->StreamClose(handle); + if (!deferFree) + networkBulkStreamFree(stream); + networkConnectionProtocolFailure(this); + return QUIC_STATUS_SUCCESS; + } + if (!stream->outbound || !event->START_COMPLETE.ID || + (event->START_COMPLETE.ID & 3U) != 2U) + { + this->api->StreamShutdown(handle, + QUIC_STREAM_SHUTDOWN_FLAG_ABORT, 0); + networkConnectionProtocolFailure(this); + return QUIC_STATUS_SUCCESS; + } + + uint64_t sessionEpoch; + LG_LOCK(this->lock); + const uint16_t serviceMask = UINT16_C(1) << stream->service; + const bool stateValid = + !atomic_load_explicit(&this->stopping, memory_order_acquire) && + atomic_load_explicit(&this->sessionValid, memory_order_acquire) && + this->outboundBulk[stream->service] == stream && + (this->receivedServices & serviceMask) && + (this->bulkServices & serviceMask); + sessionEpoch = this->sessionEpoch; + LG_UNLOCK(this->lock); + if (!stateValid) + { + this->api->StreamShutdown(handle, + QUIC_STREAM_SHUTDOWN_FLAG_ABORT, 0); + return QUIC_STATUS_SUCCESS; + } + + LGNetStreamBinding binding; + lgNetStreamBindingInit(&binding, stream->service, LG_NET_ROLE_CLIENT, + event->START_COMPLETE.ID, sessionEpoch, + LG_NET_STREAM_BINDING_UNIDIRECTIONAL | + LG_NET_STREAM_BINDING_BULK); + struct NetworkSend * send = malloc( + sizeof(*send) + LG_NET_STREAM_BINDING_WIRE_SIZE); + if (!send || !lgNetStreamBindingEncode(send->data, + LG_NET_STREAM_BINDING_WIRE_SIZE, &binding)) + { + free(send); + networkConnectionProtocolFailure(this); + return QUIC_STATUS_SUCCESS; + } + + send->buffer.Buffer = send->data; + send->buffer.Length = LG_NET_STREAM_BINDING_WIRE_SIZE; + send->service = stream->service; + send->length = LG_NET_STREAM_BINDING_WIRE_SIZE; + send->delivery = NETWORK_PACKET_DELIVERY_BULK; + send->accounted = false; + + uint16_t ready = 0; + LG_LOCK(this->lock); + const bool sendValid = + !atomic_load_explicit(&this->stopping, memory_order_acquire) && + atomic_load_explicit(&this->sessionValid, memory_order_acquire) && + this->sessionEpoch == sessionEpoch && + this->outboundBulk[stream->service] == stream; + const QUIC_STATUS status = sendValid ? + this->api->StreamSend(handle, &send->buffer, 1, + QUIC_SEND_FLAG_NONE, send) : QUIC_STATUS_ABORTED; + if (QUIC_SUCCEEDED(status)) + { + stream->streamID = event->START_COMPLETE.ID; + stream->bound = true; + if (stream->openingReturned) + ready = networkConnectionReadyServicesNL( + this, NETWORK_PACKET_DELIVERY_BULK); + } + LG_UNLOCK(this->lock); + + if (QUIC_FAILED(status)) + { + free(send); + if (sendValid) + networkConnectionProtocolFailure(this); + else + this->api->StreamShutdown(handle, + QUIC_STREAM_SHUTDOWN_FLAG_ABORT, 0); + return QUIC_STATUS_SUCCESS; + } + + networkConnectionNotifySendReady(this, 0, 0, ready); + return QUIC_STATUS_SUCCESS; + } + + case QUIC_STREAM_EVENT_RECEIVE: + if (stream->outbound || !networkBulkStreamReceive(stream, + event->RECEIVE.Buffers, event->RECEIVE.BufferCount)) + networkConnectionProtocolFailure(this); + return QUIC_STATUS_SUCCESS; + + case QUIC_STREAM_EVENT_SEND_COMPLETE: + { + struct NetworkSend * send = event->SEND_COMPLETE.ClientContext; + LG_LOCK(this->lock); + const uint16_t ready = networkConnectionReleaseSendNL(this, send); + LG_UNLOCK(this->lock); + free(send); + networkConnectionNotifySendReady(this, 0, 0, ready); + return QUIC_STATUS_SUCCESS; + } + + case QUIC_STREAM_EVENT_PEER_SEND_SHUTDOWN: + case QUIC_STREAM_EVENT_PEER_SEND_ABORTED: + case QUIC_STREAM_EVENT_PEER_RECEIVE_ABORTED: + if (!atomic_load_explicit(&this->stopping, memory_order_acquire)) + networkConnectionProtocolFailure(this); + return QUIC_STATUS_SUCCESS; + + case QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE: + { + LG_LOCK(this->lock); + networkBulkStreamDetachNL(stream); + const bool deferFree = + stream->outbound && !stream->openingReturned; + stream->shutdownComplete = deferFree; + LG_UNLOCK(this->lock); + this->api->StreamClose(handle); + if (!deferFree) + networkBulkStreamFree(stream); + return QUIC_STATUS_SUCCESS; + } + + default: + return QUIC_STATUS_SUCCESS; + } +} + static QUIC_STATUS QUIC_API networkRejectedStreamCallback(HQUIC handle, void * opaque, QUIC_STREAM_EVENT * event) { @@ -1732,10 +2475,13 @@ bool networkConnectionCreate(NetworkConnection ** result, atomic_init(&this->stopping, false); atomic_init(&this->sessionValid, false); + uint16_t offeredBulkStreams = 0; for (uint32_t index = 0; index < params->capabilityCount; ++index) { const LGNetCapability * capability = ¶ms->capabilities[index]; - if (!networkCapabilityOfferValid(capability)) + if (!networkCapabilityOfferValid(capability) || + capability->maxUnidirectionalStreams > + NETWORK_MAX_PEER_STREAMS - offeredBulkStreams) goto fail; const uint16_t serviceMask = UINT16_C(1) << capability->service; @@ -1743,6 +2489,7 @@ bool networkConnectionCreate(NetworkConnection ** result, goto fail; this->offeredCapabilities[capability->service] = *capability; this->offeredServices |= serviceMask; + offeredBulkStreams += capability->maxUnidirectionalStreams; ++this->offeredCapabilityCount; } @@ -1900,6 +2647,9 @@ LG_TransportStatus networkConnectionConnect(NetworkConnection * this, this->receivedServices = 0; this->reliableServices = 0; this->datagramServices = 0; + this->bulkServices = 0; + this->maxPeerBulkStreams = 0; + this->peerBulkStreams = 0; this->maxPayloadLength = NETWORK_MAX_CONTROL_PAYLOAD; this->maxDatagramPayload = 0; this->maxDatagramSendLength = 0; @@ -1907,6 +2657,10 @@ LG_TransportStatus networkConnectionConnect(NetworkConnection * this, networkConnectionResetSendAccountingNL(this); networkConnectionClearAuthProofNL(this); memset(this->serviceVersions, 0, sizeof(this->serviceVersions)); + memset(this->serviceMaxUnidirectional, 0, + sizeof(this->serviceMaxUnidirectional)); + memset(this->servicePeerBulkStreams, 0, + sizeof(this->servicePeerBulkStreams)); memset(this->serviceMaxPayload, 0, sizeof(this->serviceMaxPayload)); memset(this->serviceMaxDatagram, 0, sizeof(this->serviceMaxDatagram)); lgNetStreamParserReset(&this->receiveParser); @@ -2047,7 +2801,7 @@ bool networkConnectionServiceAvailable(NetworkConnection * this, if (!networkConnectionValid(this) || !networkConnectionServiceValid(service) || required & ~(LG_NET_CAPABILITY_RELIABLE | - LG_NET_CAPABILITY_DATAGRAM)) + LG_NET_CAPABILITY_DATAGRAM | LG_NET_CAPABILITY_BULK)) return false; LG_LOCK(this->lock); @@ -2057,7 +2811,9 @@ bool networkConnectionServiceAvailable(NetworkConnection * this, (!(required & LG_NET_CAPABILITY_RELIABLE) || (this->reliableServices & serviceMask)) && (!(required & LG_NET_CAPABILITY_DATAGRAM) || - (this->datagramServices & serviceMask)); + (this->datagramServices & serviceMask)) && + (!(required & LG_NET_CAPABILITY_BULK) || + (this->bulkServices & serviceMask)); LG_UNLOCK(this->lock); return available; } @@ -2149,7 +2905,8 @@ LG_TransportStatus networkConnectionSendDatagram(NetworkConnection * this, packetSize > this->maxDatagramSendLength) { const struct NetworkSendLimits limits = - networkConnectionSendLimits(service, true); + networkConnectionSendLimits( + service, NETWORK_PACKET_DELIVERY_DATAGRAM); if (packetSize <= limits.bytes && packetSize <= NETWORK_SEND_DATAGRAM_BYTES_MAX) { @@ -2163,7 +2920,8 @@ LG_TransportStatus networkConnectionSendDatagram(NetworkConnection * this, } const LG_TransportStatus reserveStatus = - networkConnectionReserveSendNL(this, service, true, packetSize); + networkConnectionReserveSendNL(this, service, + NETWORK_PACKET_DELIVERY_DATAGRAM, packetSize); if (reserveStatus != LG_TRANSPORT_OK) { LG_UNLOCK(this->lock); @@ -2175,13 +2933,14 @@ LG_TransportStatus networkConnectionSendDatagram(NetworkConnection * this, { const struct NetworkSend rollback = { - .service = service, - .length = (uint32_t)packetSize, - .datagram = true, + .service = service, + .length = (uint32_t)packetSize, + .delivery = NETWORK_PACKET_DELIVERY_DATAGRAM, + .accounted = true, }; const uint16_t ready = networkConnectionReleaseSendNL(this, &rollback); LG_UNLOCK(this->lock); - networkConnectionNotifySendReady(this, 0, ready); + networkConnectionNotifySendReady(this, 0, ready, 0); return LG_TRANSPORT_ERROR; } @@ -2189,7 +2948,8 @@ LG_TransportStatus networkConnectionSendDatagram(NetworkConnection * this, parameters.sequence = ++this->datagramSendSequence; send->service = service; send->length = (uint32_t)packetSize; - send->datagram = true; + send->delivery = NETWORK_PACKET_DELIVERY_DATAGRAM; + send->accounted = true; size_t wireSize; if (!lgNetPacketConstruct(send->data, packetSize, &wireSize, ¶meters, payload, payloadLength) || wireSize != packetSize) @@ -2198,7 +2958,7 @@ LG_TransportStatus networkConnectionSendDatagram(NetworkConnection * this, const uint16_t ready = networkConnectionReleaseSendNL(this, send); LG_UNLOCK(this->lock); free(send); - networkConnectionNotifySendReady(this, 0, ready); + networkConnectionNotifySendReady(this, 0, ready, 0); return LG_TRANSPORT_ERROR; } @@ -2220,7 +2980,150 @@ LG_TransportStatus networkConnectionSendDatagram(NetworkConnection * this, if (QUIC_FAILED(status)) { free(send); - networkConnectionNotifySendReady(this, 0, ready); + networkConnectionNotifySendReady(this, 0, ready, 0); + return LG_TRANSPORT_ERROR; + } + return LG_TRANSPORT_OK; +} + +LG_TransportStatus networkConnectionSendBulk(NetworkConnection * this, + LGNetService service, uint16_t messageType, uint16_t serviceVersion, + LGNetEnvelopeFlags flags, uint64_t componentEpoch, uint64_t requestID, + const void * payload, uint32_t payloadLength) +{ + if (!this || !networkConnectionValid(this)) + return LG_TRANSPORT_DISCONNECTED; + + LGNetEnvelope routeEnvelope; + lgNetEnvelopeInit(&routeEnvelope, service, messageType, serviceVersion, + LG_NET_MESSAGE_VERSION_CURRENT); + if ((payloadLength && !payload) || + !networkConnectionServiceValid(service) || + flags & (LG_NET_ENVELOPE_RELIABLE | LG_NET_ENVELOPE_DATAGRAM | + LG_NET_ENVELOPE_EARLY_DATA) || + !lgNetEnvelopeRouteValid(&routeEnvelope, LG_NET_ROLE_CLIENT, + LG_NET_ROUTE_DELIVERY_BULK) || + payloadLength > LG_NET_MAX_PAYLOAD_LENGTH) + return LG_TRANSPORT_ERROR; + + const size_t packetSize = + LG_NET_ENVELOPE_WIRE_SIZE + payloadLength; + const struct NetworkSendLimits limits = + networkConnectionSendLimits(service, NETWORK_PACKET_DELIVERY_BULK); + if (packetSize > limits.bytes || + packetSize > NETWORK_SEND_BULK_BYTES_MAX) + return LG_TRANSPORT_ERROR; + + LGNetPacketParameters parameters; + lgNetPacketParametersInit(¶meters, service, messageType, serviceVersion, + LG_NET_MESSAGE_VERSION_CURRENT); + parameters.flags = flags | LG_NET_ENVELOPE_FINAL; + parameters.componentEpoch = componentEpoch; + parameters.requestID = requestID; + +retry: + LG_LOCK(this->lock); + const uint16_t serviceMask = UINT16_C(1) << service; + if (!this->connection || + atomic_load_explicit(&this->stopping, memory_order_acquire) || + !atomic_load_explicit(&this->sessionValid, memory_order_acquire)) + { + LG_UNLOCK(this->lock); + return LG_TRANSPORT_DISCONNECTED; + } + if (!(this->receivedServices & serviceMask) || + !(this->bulkServices & serviceMask) || + !this->serviceMaxUnidirectional[service] || + this->serviceVersions[service] != serviceVersion || + payloadLength > this->serviceMaxPayload[service]) + { + LG_UNLOCK(this->lock); + return LG_TRANSPORT_UNAVAILABLE; + } + + struct NetworkBulkStream * stream = this->outboundBulk[service]; + if (!stream) + { + LG_UNLOCK(this->lock); + const LG_TransportStatus openStatus = + networkConnectionOpenBulkStream(this, service, packetSize); + if (openStatus == LG_TRANSPORT_OK) + goto retry; + return openStatus; + } + if (!stream->bound) + { + stream->openingReturned = true; + networkConnectionMarkSendBlockedNL(this, service, + NETWORK_PACKET_DELIVERY_BULK, packetSize); + LG_UNLOCK(this->lock); + return LG_TRANSPORT_UNAVAILABLE; + } + if (stream->sendSequence == UINT64_MAX) + { + LG_UNLOCK(this->lock); + return LG_TRANSPORT_ERROR; + } + + const LG_TransportStatus reserveStatus = + networkConnectionReserveSendNL(this, service, + NETWORK_PACKET_DELIVERY_BULK, packetSize); + if (reserveStatus != LG_TRANSPORT_OK) + { + LG_UNLOCK(this->lock); + return reserveStatus; + } + + struct NetworkSend * send = malloc(sizeof(*send) + packetSize); + if (!send) + { + const struct NetworkSend rollback = + { + .service = service, + .length = (uint32_t)packetSize, + .delivery = NETWORK_PACKET_DELIVERY_BULK, + .accounted = true, + }; + const uint16_t ready = networkConnectionReleaseSendNL(this, &rollback); + LG_UNLOCK(this->lock); + networkConnectionNotifySendReady(this, 0, 0, ready); + return LG_TRANSPORT_ERROR; + } + + parameters.sessionEpoch = this->sessionEpoch; + parameters.sequence = ++stream->sendSequence; + send->service = service; + send->length = (uint32_t)packetSize; + send->delivery = NETWORK_PACKET_DELIVERY_BULK; + send->accounted = true; + size_t wireSize; + if (!lgNetPacketConstruct(send->data, packetSize, &wireSize, + ¶meters, payload, payloadLength) || wireSize != packetSize) + { + --stream->sendSequence; + const uint16_t ready = networkConnectionReleaseSendNL(this, send); + LG_UNLOCK(this->lock); + free(send); + networkConnectionNotifySendReady(this, 0, 0, ready); + return LG_TRANSPORT_ERROR; + } + + send->buffer.Buffer = send->data; + send->buffer.Length = (uint32_t)packetSize; + const QUIC_STATUS status = this->api->StreamSend(stream->handle, + &send->buffer, 1, QUIC_SEND_FLAG_NONE, send); + uint16_t ready = 0; + if (QUIC_FAILED(status)) + { + --stream->sendSequence; + ready = networkConnectionReleaseSendNL(this, send); + } + LG_UNLOCK(this->lock); + + if (QUIC_FAILED(status)) + { + free(send); + networkConnectionNotifySendReady(this, 0, 0, ready); return LG_TRANSPORT_ERROR; } return LG_TRANSPORT_OK; diff --git a/client/transports/Network/connection.h b/client/transports/Network/connection.h index 04238ab1..19f5c879 100644 --- a/client/transports/Network/connection.h +++ b/client/transports/Network/connection.h @@ -31,6 +31,14 @@ typedef struct NetworkConnection NetworkConnection; +typedef enum NetworkPacketDelivery +{ + NETWORK_PACKET_DELIVERY_RELIABLE, + NETWORK_PACKET_DELIVERY_DATAGRAM, + NETWORK_PACKET_DELIVERY_BULK, +} +NetworkPacketDelivery; + typedef void (*NetworkTrustDecisionFn)(void * opaque, bool trusted); typedef bool (*NetworkTrustPromptFn)(void * opaque, const char * endpoint, const char * fingerprint, NetworkTrustDecisionFn decision, @@ -38,11 +46,11 @@ typedef bool (*NetworkTrustPromptFn)(void * opaque, const char * endpoint, typedef void (*NetworkTrustCancelFn)(void * opaque); /* The packet view and payload are borrowed for the duration of the callback. */ typedef void (*NetworkPacketFn)(void * opaque, - const LGNetPacketView * packet, bool datagram); + const LGNetPacketView * packet, NetworkPacketDelivery delivery); typedef void (*NetworkConnectionStateFn)(void * opaque, bool connected, LG_TransportStatus reason); typedef void (*NetworkSendReadyFn)(void * opaque, LGNetService service, - bool datagram); + NetworkPacketDelivery delivery); typedef struct NetworkConnectionParams { @@ -85,3 +93,7 @@ LG_TransportStatus networkConnectionSendDatagram( uint16_t messageType, uint16_t serviceVersion, LGNetEnvelopeFlags flags, uint64_t componentEpoch, uint64_t requestID, const void * payload, uint32_t payloadLength, bool discardIfBlocked); +LG_TransportStatus networkConnectionSendBulk(NetworkConnection * connection, + LGNetService service, uint16_t messageType, uint16_t serviceVersion, + LGNetEnvelopeFlags flags, uint64_t componentEpoch, uint64_t requestID, + const void * payload, uint32_t payloadLength); diff --git a/client/transports/Network/network.c b/client/transports/Network/network.c index c9a3027b..bbfa5d01 100644 --- a/client/transports/Network/network.c +++ b/client/transports/Network/network.c @@ -20,6 +20,7 @@ #include "network.h" #include "audio.h" +#include "clipboard.h" #include "connection.h" #include "control.h" #include "cursor.h" @@ -43,13 +44,18 @@ #include #include -#define NETWORK_ADDRESS_MAX 255U -#define NETWORK_ENDPOINT_TEXT_SIZE (NETWORK_ADDRESS_MAX + 9U) -#define NETWORK_MAX_PAYLOAD \ +#define NETWORK_ADDRESS_MAX 255U +#define NETWORK_ENDPOINT_TEXT_SIZE (NETWORK_ADDRESS_MAX + 9U) +#define NETWORK_MAX_PAYLOAD \ (LG_NET_CURSOR_SHAPE_HEADER_WIRE_SIZE + LG_NET_CURSOR_MAX_SHAPE_LENGTH) -#define NETWORK_MAX_DATAGRAM 1136U -#define NETWORK_AUDIO_MAX_PAYLOAD \ +#define NETWORK_MAX_DATAGRAM 1136U +#define NETWORK_AUDIO_MAX_PAYLOAD \ (LG_NET_AUDIO_DATA_HEADER_WIRE_SIZE + 65536U) +#define NETWORK_TRANSFER_CHUNK (256U * 1024U) +#define NETWORK_CLIPBOARD_MAX_PAYLOAD \ + (LG_NET_CLIPBOARD_CHUNK_HEADER_WIRE_SIZE + NETWORK_TRANSFER_CHUNK) +#define NETWORK_FILE_MAX_PAYLOAD \ + (LG_NET_FILE_CHUNK_HEADER_WIRE_SIZE + NETWORK_TRANSFER_CHUNK) static const LGNetCapability NETWORK_CAPABILITIES[] = { @@ -101,6 +107,30 @@ static const LGNetCapability NETWORK_CAPABILITIES[] = .maxBidirectionalStreams = 0, .maxUnidirectionalStreams = 0, }, + { + .service = LG_NET_SERVICE_CLIPBOARD, + .versionMin = LG_NET_CLIPBOARD_VERSION_MIN, + .versionMax = LG_NET_CLIPBOARD_VERSION_CURRENT, + .flags = LG_NET_CAPABILITY_RELIABLE | + LG_NET_CAPABILITY_BULK, + .features = 0, + .maxPayloadLength = NETWORK_CLIPBOARD_MAX_PAYLOAD, + .maxDatagramPayload = 0, + .maxBidirectionalStreams = 0, + .maxUnidirectionalStreams = 1, + }, + { + .service = LG_NET_SERVICE_FILE, + .versionMin = LG_NET_FILE_VERSION_MIN, + .versionMax = LG_NET_FILE_VERSION_CURRENT, + .flags = LG_NET_CAPABILITY_RELIABLE | + LG_NET_CAPABILITY_BULK, + .features = 0, + .maxPayloadLength = NETWORK_FILE_MAX_PAYLOAD, + .maxDatagramPayload = 0, + .maxBidirectionalStreams = 0, + .maxUnidirectionalStreams = 1, + }, { .service = LG_NET_SERVICE_CONTROL, .versionMin = LG_NET_CONTROL_VERSION_MIN, @@ -125,6 +155,7 @@ struct LG_Transport { NetworkConnection * connection; NetworkAudio * audio; + NetworkClipboard * clipboard; NetworkControl * control; NetworkCursor * cursor; NetworkInput * input; @@ -341,6 +372,7 @@ static void networkConnectionState(void * opaque, bool connected, networkInputConnectionState(this->input, connected); networkAudioConnectionState(this->audio, connected); + networkClipboardConnectionState(this->clipboard, connected); networkControlConnectionState(this->control, connected); networkRecoveryConnectionState(this->recovery, connected); const bool cursorAvailable = @@ -359,28 +391,39 @@ static void networkConnectionState(void * opaque, bool connected, } static void networkPacket(void * opaque, const LGNetPacketView * packet, - bool datagram) + NetworkPacketDelivery delivery) { - LG_Transport * this = opaque; - bool valid; + LG_Transport * this = opaque; + const bool datagram = + delivery == NETWORK_PACKET_DELIVERY_DATAGRAM; + bool valid; switch (packet->envelope.service) { case LG_NET_SERVICE_RECOVERY: - valid = networkRecoveryPacket(this->recovery, packet, datagram); + valid = delivery != NETWORK_PACKET_DELIVERY_BULK && + networkRecoveryPacket(this->recovery, packet, datagram); break; case LG_NET_SERVICE_CURSOR: - valid = networkCursorPacket(this->cursor, packet, datagram); + valid = delivery != NETWORK_PACKET_DELIVERY_BULK && + networkCursorPacket(this->cursor, packet, datagram); break; case LG_NET_SERVICE_INPUT: - valid = networkInputPacket(this->input, packet, datagram); + valid = delivery != NETWORK_PACKET_DELIVERY_BULK && + networkInputPacket(this->input, packet, datagram); break; case LG_NET_SERVICE_AUDIO: - valid = datagram ? - networkAudioDatagram(this->audio, packet) : - networkAudioPacket(this->audio, packet); + valid = delivery != NETWORK_PACKET_DELIVERY_BULK && + (datagram ? networkAudioDatagram(this->audio, packet) : + networkAudioPacket(this->audio, packet)); + break; + case LG_NET_SERVICE_CLIPBOARD: + case LG_NET_SERVICE_FILE: + valid = delivery != NETWORK_PACKET_DELIVERY_DATAGRAM && + networkClipboardPacket(this->clipboard, packet, delivery); break; case LG_NET_SERVICE_CONTROL: - valid = networkControlPacket(this->control, packet, datagram); + valid = delivery != NETWORK_PACKET_DELIVERY_BULK && + networkControlPacket(this->control, packet, datagram); break; default: valid = false; @@ -397,11 +440,17 @@ static void networkPacket(void * opaque, const LGNetPacketView * packet, } static void networkSendReady(void * opaque, LGNetService service, - bool datagram) + NetworkPacketDelivery delivery) { - LG_Transport * this = opaque; - if (service == LG_NET_SERVICE_AUDIO) + LG_Transport * this = opaque; + const bool datagram = + delivery == NETWORK_PACKET_DELIVERY_DATAGRAM; + if (service == LG_NET_SERVICE_AUDIO && + delivery != NETWORK_PACKET_DELIVERY_BULK) networkAudioSendReady(this->audio, datagram); + if ((service == LG_NET_SERVICE_CLIPBOARD || + service == LG_NET_SERVICE_FILE) && !datagram) + networkClipboardSendReady(this->clipboard, service, delivery); lgSignalEvent(this->frameEvent); } @@ -463,6 +512,7 @@ static bool networkCreate(LG_Transport ** result) if (!networkConnectionCreate(&this->connection, ¶ms)) goto fail; if (!networkAudioCreate(&this->audio, this->connection) || + !networkClipboardCreate(&this->clipboard, this->connection) || !networkControlCreate(&this->control, this->connection) || !networkCursorCreate(&this->cursor, this->connection) || !networkInputCreate(&this->input, this->connection) || @@ -477,6 +527,7 @@ fail: networkInputDestroy(&this->input); networkCursorDestroy(&this->cursor); networkControlDestroy(&this->control); + networkClipboardDestroy(&this->clipboard); networkAudioDestroy(&this->audio); networkConnectionDestroy(&this->connection); if (this->frameEvent) @@ -512,6 +563,7 @@ static void networkDestroy(LG_Transport ** transport) networkInputDestroy(&this->input); networkCursorDestroy(&this->cursor); networkControlDestroy(&this->control); + networkClipboardDestroy(&this->clipboard); networkAudioDestroy(&this->audio); networkConnectionDestroy(&this->connection); lgFreeEvent(this->frameEvent); @@ -542,6 +594,7 @@ static void networkDisconnect(LG_Transport * this) { networkInputConnectionState(this->input, false); networkAudioConnectionState(this->audio, false); + networkClipboardConnectionState(this->clipboard, false); networkControlConnectionState(this->control, false); networkRecoveryConnectionState(this->recovery, false); networkCursorConnectionState(this->cursor, false); @@ -728,9 +781,14 @@ static const LG_AudioOps * networkGetAudioOps(LG_Transport * this, static const LG_ClipboardOps * networkGetClipboardOps(LG_Transport * this, void ** opaque) { - (void)this; *opaque = NULL; - return NULL; + if (!networkConnectionServiceAvailable(this->connection, + LG_NET_SERVICE_CLIPBOARD, LG_NET_CAPABILITY_RELIABLE) || + !networkConnectionServiceAvailable(this->connection, + LG_NET_SERVICE_FILE, LG_NET_CAPABILITY_RELIABLE)) + return NULL; + *opaque = this->clipboard; + return networkClipboardGetOps(); } static LG_TransportStatus networkSendControl(LG_Transport * this, diff --git a/idd/LGIdd/LGIdd.vcxproj b/idd/LGIdd/LGIdd.vcxproj index 3f221154..19df3eab 100644 --- a/idd/LGIdd/LGIdd.vcxproj +++ b/idd/LGIdd/LGIdd.vcxproj @@ -89,6 +89,7 @@ + @@ -175,6 +176,7 @@ + diff --git a/idd/LGIdd/LGIdd.vcxproj.filters b/idd/LGIdd/LGIdd.vcxproj.filters index 26acc6fa..b471c256 100644 --- a/idd/LGIdd/LGIdd.vcxproj.filters +++ b/idd/LGIdd/LGIdd.vcxproj.filters @@ -289,6 +289,9 @@ Transport\Network + + Transport\Network + Transport\Network @@ -483,6 +486,9 @@ Transport\Network + + Transport\Network + Transport\Network diff --git a/idd/LGIdd/transport/network/CNetworkClipboard.cpp b/idd/LGIdd/transport/network/CNetworkClipboard.cpp new file mode 100644 index 00000000..aeda85b0 --- /dev/null +++ b/idd/LGIdd/transport/network/CNetworkClipboard.cpp @@ -0,0 +1,3487 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "transport/network/CNetworkClipboard.h" + +#include "CSRWLock.h" + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace +{ + constexpr size_t MAX_SEND_COUNT = 128; + constexpr size_t MAX_SEND_BYTES = 8U * 1024U * 1024U; + constexpr size_t MAX_TARGET_COUNT = 128; + constexpr size_t MAX_TARGET_BYTES = 8U * 1024U * 1024U; + constexpr size_t PROCESS_MAX = 64; + constexpr size_t FORMAT_COUNT = LG_NET_CLIPBOARD_KVMFR_FORMAT_COUNT; + constexpr size_t MAX_ACQUISITIONS = KVMFR_CLIPBOARD_FILE_MAX_ACQUISITIONS; + constexpr size_t MAX_REQUESTS = KVMFR_CLIPBOARD_FILE_MAX_REQUESTS; + constexpr uint32_t MAX_CHUNK_LENGTH = KVMFR_CLIPBOARD_STREAM_SLOT_BYTES; + constexpr uint32_t MAX_FILE_READ_LENGTH = KVMFR_CLIPBOARD_FILE_READ_BYTES; + constexpr uint32_t MIN_CHUNK_LENGTH = + (MAX_CHUNK_LENGTH + MAX_SEND_COUNT - 3U) / (MAX_SEND_COUNT - 2U); + + constexpr LGNetClipboardClaimFlags CLAIM_FLAGS = + LG_NET_CLIPBOARD_CLAIM_READ | LG_NET_CLIPBOARD_CLAIM_WRITE; + + bool SamePeer( + const NetworkClipboardPeer& left, const NetworkClipboardPeer& right) + { + return left.client == right.client && left.generation == right.generation; + } + + uint64_t Next64(uint64_t value) + { + ++value; + return value ? value : 1; + } + + bool DeadlineReached(uint64_t now, uint64_t deadline) + { + return static_cast(now - deadline) >= 0; + } + + bool DeliveryFlags(const LGNetEnvelope& envelope, + LGNetEnvelopeFlags expected, bool bulk) + { + constexpr LGNetEnvelopeFlags MASK = LG_NET_ENVELOPE_RESPONSE | + LG_NET_ENVELOPE_FINAL | LG_NET_ENVELOPE_RELIABLE | + LG_NET_ENVELOPE_DATAGRAM | LG_NET_ENVELOPE_ACK_REQUIRED | + LG_NET_ENVELOPE_ERROR | LG_NET_ENVELOPE_EARLY_DATA; + const LGNetEnvelopeFlags delivery = + bulk ? 0 : LG_NET_ENVELOPE_RELIABLE; + return (envelope.flags & MASK) == + (expected | LG_NET_ENVELOPE_FINAL | delivery); + } + + bool ValidOffer(const KVMFRClipboardMessage& record) + { + return record.version == KVMFR_CLIPBOARD_VERSION && + record.type == KVMFR_CLIPBOARD_MESSAGE_OFFER && + record.clipboardGeneration && record.token && + !(record.token & ~KVMFR_CLIPBOARD_FORMAT_MASK_ALL) && !record.sequence && + !record.transfer && !record.offset && !record.size && !record.format && + !record.flags && !record.length; + } + + bool ValidClear(const KVMFRClipboardMessage& record) + { + return record.version == KVMFR_CLIPBOARD_VERSION && + record.type == KVMFR_CLIPBOARD_MESSAGE_CLEAR && + record.clipboardGeneration && !record.token && !record.sequence && + !record.transfer && !record.offset && !record.size && !record.format && + !record.flags && !record.length; + } + + bool ValidRequest(const KVMFRClipboardMessage& record, bool helper) + { + const bool transfer = helper ? + kvmfrClipboardTransferFromHelper(record.transfer) : + kvmfrClipboardTransferFromClient(record.transfer); + return record.version == KVMFR_CLIPBOARD_VERSION && + record.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST && + record.clipboardGeneration && transfer && + kvmfrClipboardRepresentationFormatValid(record.format) && + !record.sequence && !record.offset && !record.size && !record.flags && + !record.token && !record.length; + } + + bool ValidCancel(const KVMFRClipboardMessage& record) + { + return record.version == KVMFR_CLIPBOARD_VERSION && + record.type == KVMFR_CLIPBOARD_MESSAGE_CANCEL && + record.clipboardGeneration && record.transfer && + (!record.format || + kvmfrClipboardRepresentationFormatValid(record.format)) && + !record.sequence && !record.offset && !record.size && !record.flags && + !record.length; + } + + bool ValidData(const KVMFRClipboardMessage& record) + { + return record.version == KVMFR_CLIPBOARD_VERSION && + record.type == KVMFR_CLIPBOARD_MESSAGE_DATA && + record.clipboardGeneration && record.transfer && + kvmfrClipboardRepresentationFormatValid(record.format) && + record.length <= KVMFR_CLIPBOARD_DATA_BYTES && + (record.length || record.flags) && + !(record.flags & + ~(KVMFR_CLIPBOARD_FLAG_BEGIN | KVMFR_CLIPBOARD_FLAG_END)) && + !record.token && record.offset <= UINT64_MAX - record.length; + } +} + +struct CNetworkClipboard::Impl +{ + enum class Direction + { + REMOTE_REQUESTER, + HELPER_REQUESTER, + }; + + struct PeerState + { + NetworkClipboardPeer peer = {}; + NetworkClipboardPeerInfo info = {}; + uint64_t lastClaimEpoch = 0; + }; + + struct RemoteOffer + { + uint64_t offerID = 0; + uint64_t serial = 0; + uint32_t formatCount = 0; + uint32_t received = 0; + uint32_t formatMask = 0; + bool fileOffer = false; + bool complete = false; + bool published = false; + std::array formats = {}; + + void Clear() + { + *this = {}; + } + }; + + struct ClipboardTransfer + { + NetworkClipboardPeer peer = {}; + Direction direction = Direction::REMOTE_REQUESTER; + uint64_t claimEpoch = 0; + uint64_t wireID = 0; + uint64_t localID = 0; + uint64_t offerID = 0; + uint64_t offset = 0; + uint64_t sizeHint = KVMFR_CLIPBOARD_SIZE_UNKNOWN; + uint32_t sequence = 0; + uint32_t formatIndex = 0; + uint32_t maxChunk = MAX_CHUNK_LENGTH; + KVMFRClipboardFormat format = KVMFR_CLIPBOARD_FORMAT_NONE; + bool bulk = false; + bool began = false; + }; + + struct FileAcquisition + { + NetworkClipboardPeer peer = {}; + Direction direction = Direction::REMOTE_REQUESTER; + uint64_t claimEpoch = 0; + uint64_t offerID = 0; + uint64_t wireID = 0; + uint64_t localID = 0; + uint64_t leaseEpoch = 0; + bool active = false; + }; + + struct FileTransfer + { + NetworkClipboardPeer peer = {}; + Direction direction = Direction::REMOTE_REQUESTER; + uint64_t claimEpoch = 0; + uint64_t wireID = 0; + uint64_t localID = 0; + uint64_t offerID = 0; + uint64_t entryID = 0; + uint64_t requestOffset = 0; + uint64_t responseOffset = 0; + uint64_t sizeHint = KVMFR_CLIPBOARD_SIZE_UNKNOWN; + uint32_t sequence = 0; + uint32_t length = 0; + uint32_t maxChunk = MAX_CHUNK_LENGTH; + KVMFRClipboardFileOperation operation = 0; + bool bulk = false; + bool began = false; + }; + + struct SendItem + { + uint64_t serial = 0; + NetworkClipboardPeer peer = {}; + bool bulk = false; + LGNetService service = 0; + uint16_t messageType = 0; + LGNetEnvelopeFlags flags = LG_NET_ENVELOPE_FINAL; + uint64_t componentEpoch = 0; + uint64_t requestID = 0; + std::vector payload = {}; + }; + + struct TargetItem + { + uint64_t serial = 0; + KVMFRClipboardMessage record = {}; + std::vector data = {}; + bool wasBlocked = false; + }; + + CSRWLock m_lifecycleLock; + CSRWLock m_processLock; + CSRWLock m_senderCallbackLock; + CSRWLock m_targetCallbackLock; + CSRWLock m_lock; + + INetworkClipboardSender * m_sender = nullptr; + IClipboardTarget * m_target = nullptr; + bool m_started = false; + bool m_stateKnown = false; + bool m_available = false; + bool m_failed = false; + bool m_failureNotified = false; + bool m_targetCallbackActive = false; + bool m_sourceBlocked = false; + bool m_receiveReadyPending = false; + LGNetService m_sourceBlockedService = 0; + uint64_t m_sourceBlockedTransfer = 0; + uint32_t m_generation = 0; + uint64_t m_nextSerial = 0; + uint64_t m_nextLocal = 0; + uint64_t m_nextWire = 0; + uint64_t m_nextEpoch = 0; + + std::vector m_peers = {}; + NetworkClipboardPeer m_owner = {}; + uint64_t m_ownerID = 0; + uint64_t m_claimEpoch = 0; + uint64_t m_claimSerial = 0; + uint64_t m_remoteClaimSerial = 0; + uint64_t m_remoteOfferSerial = 0; + uint64_t m_claimDeadline = 0; + uint32_t m_claimLeaseMs = 0; + RemoteOffer m_remoteOffer = {}; + bool m_cachedValid = false; + KVMFRClipboardMessage m_cachedRecord = {}; + std::vector m_clipboardTransfers = {}; + std::vector m_fileAcquisitions = {}; + std::vector m_fileTransfers = {}; + std::deque m_sends = {}; + size_t m_sendBytes = 0; + std::deque m_targets = {}; + size_t m_targetBytes = 0; + + PeerState * FindPeerNL(const NetworkClipboardPeer& peer) + { + for (PeerState& state : m_peers) + if (SamePeer(state.peer, peer)) + return &state; + return nullptr; + } + + const PeerState * FindPeerNL(const NetworkClipboardPeer& peer) const + { + for (const PeerState& state : m_peers) + if (SamePeer(state.peer, peer)) + return &state; + return nullptr; + } + + bool IsOwnerNL(const NetworkClipboardPeer& peer, uint64_t epoch) const + { + return m_claimEpoch && SamePeer(m_owner, peer) && m_claimEpoch == epoch; + } + + uint64_t NextLocalNL(bool helper) + { + m_nextLocal = Next64(m_nextLocal) & ~KVMFR_CLIPBOARD_TRANSFER_HELPER; + if (!m_nextLocal) + m_nextLocal = 1; + return helper ? m_nextLocal | KVMFR_CLIPBOARD_TRANSFER_HELPER : m_nextLocal; + } + + uint64_t NextWireNL() + { + m_nextWire = Next64(m_nextWire); + return m_nextWire; + } + + uint64_t NextEpochNL() + { + m_nextEpoch = Next64(m_nextEpoch); + return m_nextEpoch; + } + + ClipboardTransfer * FindClipboardLocalNL(uint64_t localID) + { + for (ClipboardTransfer& transfer : m_clipboardTransfers) + if (transfer.localID == localID) + return &transfer; + return nullptr; + } + + ClipboardTransfer * FindClipboardWireNL(uint64_t wireID) + { + for (ClipboardTransfer& transfer : m_clipboardTransfers) + if (transfer.wireID == wireID) + return &transfer; + return nullptr; + } + + FileAcquisition * FindAcquisitionLocalNL(uint64_t localID) + { + for (FileAcquisition& acquisition : m_fileAcquisitions) + if (acquisition.localID == localID) + return &acquisition; + return nullptr; + } + + FileAcquisition * FindAcquisitionWireNL(uint64_t wireID) + { + for (FileAcquisition& acquisition : m_fileAcquisitions) + if (acquisition.wireID == wireID) + return &acquisition; + return nullptr; + } + + FileTransfer * FindFileLocalNL(uint64_t localID) + { + for (FileTransfer& transfer : m_fileTransfers) + if (transfer.localID == localID) + return &transfer; + return nullptr; + } + + FileTransfer * FindFileWireNL(uint64_t wireID) + { + for (FileTransfer& transfer : m_fileTransfers) + if (transfer.wireID == wireID) + return &transfer; + return nullptr; + } + + bool QueueSendBatchNL(std::vector& batch); + bool QueueTargetNL(TargetItem&& item); + bool SelectBulkNL( + const PeerState& peer, LGNetService service, uint32_t payloadLength) const; + bool PayloadFitsNL(const PeerState& peer, LGNetService service, bool bulk, + uint32_t payloadLength) const; + uint32_t ChunkLengthNL( + const PeerState& peer, LGNetService service, uint32_t headerLength, + bool bulk) const; + bool BuildSendForDeliveryNL(SendItem& item, + const NetworkClipboardPeer& peer, LGNetService service, + uint16_t messageType, LGNetEnvelopeFlags flags, uint64_t componentEpoch, + uint64_t requestID, uint32_t payloadLength, bool bulk); + bool BuildSendNL(SendItem& item, const NetworkClipboardPeer& peer, + LGNetService service, uint16_t messageType, LGNetEnvelopeFlags flags, + uint64_t componentEpoch, uint64_t requestID, uint32_t payloadLength, + bool preferBulk); + + void PumpSends(); + void PumpTargets(); + void NotifyTargetReady(); + void NotifyFailure(); + void DropOwnerNL(bool clearHelper); + void ResetTransfersNL(); + void ClearRemoteTransfersNL(uint64_t offerID); + bool PublishRemoteOfferNL(); + bool ReplayCachedNL(std::vector& batch); + + bool QueueClaimStatusNL(const NetworkClipboardPeer& peer, uint64_t epoch, + LGNetClipboardClaimCode code, LGNetClipboardClaimFlags flags, + uint32_t leaseMs, uint32_t detail); + bool QueueControlNL( + uint16_t messageType, uint64_t offerID, std::vector& batch); + bool QueueClipboardStatusNL(const NetworkClipboardPeer& peer, uint64_t epoch, + uint64_t requestID, uint64_t offerID, LGNetClipboardStatusCode status, + uint32_t detail, uint64_t processedLength, std::vector& batch); + bool QueueFileStatusNL(const NetworkClipboardPeer& peer, uint64_t epoch, + uint64_t requestID, uint64_t offerID, uint64_t entryID, + KVMFRClipboardFileError error, uint64_t transferredLength, + std::vector& batch); + bool BuildCachedOfferNL( + std::vector& batch, const KVMFRClipboardMessage& record); + + NetworkClipboardPacketResult HandleClaimNL( + PeerState& peer, const LGNetPacketView& packet, uint64_t nowMs); + NetworkClipboardPacketResult HandleClipboardNL( + PeerState& peer, const LGNetPacketView& packet, bool bulk, uint64_t nowMs); + NetworkClipboardPacketResult HandleFileNL( + PeerState& peer, const LGNetPacketView& packet, bool bulk); + + ClipboardChannelResult HandleHelperClipboardNL( + const KVMFRClipboardMessage& record, const uint8_t * data); + ClipboardChannelResult HandleHelperFileNL( + const KVMFRClipboardMessage& record, const uint8_t * data); +}; + +bool CNetworkClipboard::Impl::PayloadFitsNL(const PeerState& peer, + LGNetService service, bool bulk, uint32_t payloadLength) const +{ + LGNetCapabilityFlags flags; + uint32_t maximum; + if (service == LG_NET_SERVICE_CLIPBOARD) + { + flags = peer.info.clipboardFlags; + maximum = bulk ? peer.info.clipboardBulkPayload : + peer.info.clipboardReliablePayload; + } + else if (service == LG_NET_SERVICE_FILE) + { + flags = peer.info.fileFlags; + maximum = bulk ? peer.info.fileBulkPayload : peer.info.fileReliablePayload; + } + else + return false; + + const LGNetCapabilityFlags required = + bulk ? LG_NET_CAPABILITY_BULK : LG_NET_CAPABILITY_RELIABLE; + return (flags & required) && maximum >= payloadLength; +} + +bool CNetworkClipboard::Impl::SelectBulkNL( + const PeerState& peer, LGNetService service, uint32_t payloadLength) const +{ + return PayloadFitsNL(peer, service, true, payloadLength); +} + +uint32_t CNetworkClipboard::Impl::ChunkLengthNL( + const PeerState& peer, LGNetService service, uint32_t headerLength, + bool bulk) const +{ + LGNetCapabilityFlags flags; + uint32_t maximum; + if (service == LG_NET_SERVICE_CLIPBOARD) + { + flags = peer.info.clipboardFlags; + maximum = bulk ? peer.info.clipboardBulkPayload : + peer.info.clipboardReliablePayload; + } + else if (service == LG_NET_SERVICE_FILE) + { + flags = peer.info.fileFlags; + maximum = bulk ? peer.info.fileBulkPayload : peer.info.fileReliablePayload; + } + else + return 0; + + const LGNetCapabilityFlags required = + bulk ? LG_NET_CAPABILITY_BULK : LG_NET_CAPABILITY_RELIABLE; + if (!(flags & required) || maximum <= headerLength) + return 0; + return (std::min)(maximum - headerLength, MAX_CHUNK_LENGTH); +} + +bool CNetworkClipboard::Impl::BuildSendForDeliveryNL(SendItem& item, + const NetworkClipboardPeer& peer, LGNetService service, uint16_t messageType, + LGNetEnvelopeFlags flags, uint64_t componentEpoch, uint64_t requestID, + uint32_t payloadLength, bool bulk) +{ + const PeerState * state = FindPeerNL(peer); + if (!state) + return false; + + if (!PayloadFitsNL(*state, service, bulk, payloadLength)) + return false; + + try + { + item.payload.resize(payloadLength); + } + catch (const std::bad_alloc&) + { + return false; + } + + item.serial = Next64(m_nextSerial); + m_nextSerial = item.serial; + item.peer = peer; + item.bulk = bulk; + item.service = service; + item.messageType = messageType; + item.flags = flags | LG_NET_ENVELOPE_FINAL; + item.componentEpoch = componentEpoch; + item.requestID = requestID; + return true; +} + +bool CNetworkClipboard::Impl::BuildSendNL(SendItem& item, + const NetworkClipboardPeer& peer, LGNetService service, uint16_t messageType, + LGNetEnvelopeFlags flags, uint64_t componentEpoch, uint64_t requestID, + uint32_t payloadLength, bool preferBulk) +{ + const PeerState * state = FindPeerNL(peer); + if (!state) + return false; + + const bool bulk = preferBulk && + SelectBulkNL(*state, service, payloadLength); + return BuildSendForDeliveryNL(item, peer, service, messageType, flags, + componentEpoch, requestID, payloadLength, bulk); +} + +bool CNetworkClipboard::Impl::QueueSendBatchNL(std::vector& batch) +{ + if (batch.empty()) + return true; + if (m_failed) + return false; + + size_t bytes = 0; + for (const SendItem& item : batch) + { + if (item.payload.size() > MAX_SEND_BYTES - bytes) + return false; + bytes += item.payload.size(); + } + + if (batch.size() > MAX_SEND_COUNT - m_sends.size() || + bytes > MAX_SEND_BYTES - m_sendBytes) + return false; + + size_t inserted = 0; + try + { + for (SendItem& item : batch) + { + m_sends.emplace_back(std::move(item)); + ++inserted; + } + } + catch (const std::bad_alloc&) + { + while (inserted) + { + --inserted; + m_sends.pop_back(); + } + return false; + } + + m_sendBytes += bytes; + batch.clear(); + return true; +} + +bool CNetworkClipboard::Impl::QueueTargetNL(TargetItem&& item) +{ + const size_t bytes = sizeof(item.record) + item.data.size(); + if (m_failed || !m_started || !m_stateKnown || !m_available || + !m_generation || m_targets.size() >= MAX_TARGET_COUNT || + bytes > MAX_TARGET_BYTES - m_targetBytes) + return false; + + item.record.generation = m_generation; + item.serial = Next64(m_nextSerial); + m_nextSerial = item.serial; + try + { + m_targets.emplace_back(std::move(item)); + } + catch (const std::bad_alloc&) + { + return false; + } + m_targetBytes += bytes; + return true; +} + +void CNetworkClipboard::Impl::PumpSends() +{ + struct BlockedRoute + { + NetworkClipboardPeer peer = {}; + LGNetService service = 0; + bool bulk = false; + }; + + std::array blockedRoutes = {}; + size_t blockedCount = 0; + for (size_t count = 0; count < PROCESS_MAX; ++count) + { + SendItem action; + { + CSRWSharedLock lock(m_lock); + if (m_failed || !m_sender || m_sends.empty()) + return; + const auto available = [&blockedRoutes, blockedCount]( + const SendItem& item) + { + for (size_t index = 0; index < blockedCount; ++index) + if (SamePeer(blockedRoutes[index].peer, item.peer) && + blockedRoutes[index].service == item.service && + blockedRoutes[index].bulk == item.bulk) + return false; + return true; + }; + const auto candidate = std::find_if( + m_sends.begin(), m_sends.end(), available); + if (candidate == m_sends.end()) + return; + try + { + action = *candidate; + } + catch (const std::bad_alloc&) + { + return; + } + } + + NetworkClipboardSendResult result; + { + CSRWExclusiveLock callbackLock(m_senderCallbackLock); + INetworkClipboardSender * sender; + { + CSRWSharedLock lock(m_lock); + sender = m_sender; + } + result = sender ? + sender->SendClipboard(action.peer, action.bulk, action.service, + action.messageType, action.flags, action.componentEpoch, + action.requestID, + action.payload.empty() ? nullptr : action.payload.data(), + static_cast(action.payload.size())) : + NetworkClipboardSendResult::FAILED; + } + + { + CSRWExclusiveLock lock(m_lock); + const auto pending = std::find_if(m_sends.begin(), m_sends.end(), + [&action](const SendItem& item) + { return item.serial == action.serial; }); + if (pending == m_sends.end()) + continue; + if (result == NetworkClipboardSendResult::BLOCKED) + { + if (blockedCount < blockedRoutes.size()) + { + blockedRoutes[blockedCount].peer = action.peer; + blockedRoutes[blockedCount].service = action.service; + blockedRoutes[blockedCount].bulk = action.bulk; + ++blockedCount; + continue; + } + return; + } + if (result == NetworkClipboardSendResult::FAILED) + { + m_failed = true; + return; + } + + m_sendBytes -= pending->payload.size(); + m_sends.erase(pending); + if (m_sourceBlocked) + { + m_sourceBlocked = false; + m_sourceBlockedService = 0; + m_sourceBlockedTransfer = 0; + m_receiveReadyPending = true; + } + } + } +} + +void CNetworkClipboard::Impl::PumpTargets() +{ + for (size_t count = 0; count < PROCESS_MAX; ++count) + { + TargetItem action; + { + CSRWSharedLock lock(m_lock); + if (m_failed || !m_target || m_targets.empty()) + return; + try + { + action = m_targets.front(); + } + catch (const std::bad_alloc&) + { + return; + } + } + + ClipboardChannelResult result; + { + CSRWExclusiveLock callbackLock(m_targetCallbackLock); + IClipboardTarget * target; + { + CSRWExclusiveLock lock(m_lock); + target = m_target; + m_targetCallbackActive = target != nullptr; + } + result = target ? target->SendClipboard(action.record, + action.data.empty() ? nullptr : action.data.data()) : + ClipboardChannelResult::FAILED; + { + CSRWExclusiveLock lock(m_lock); + m_targetCallbackActive = false; + } + } + + { + CSRWExclusiveLock lock(m_lock); + if (m_targets.empty() || m_targets.front().serial != action.serial) + continue; + if (result == ClipboardChannelResult::BUSY) + { + m_targets.front().wasBlocked = true; + return; + } + if (result == ClipboardChannelResult::FAILED) + { + m_failed = true; + return; + } + + const TargetItem& item = m_targets.front(); + m_targetBytes -= sizeof(item.record) + item.data.size(); + m_targets.pop_front(); + } + } +} + +void CNetworkClipboard::Impl::NotifyTargetReady() +{ + { + CSRWSharedLock lock(m_lock); + if (m_targetCallbackActive) + return; + } + + CSRWExclusiveLock callbackLock(m_targetCallbackLock); + IClipboardTarget * target = nullptr; + { + CSRWExclusiveLock lock(m_lock); + if (m_targetCallbackActive || !m_receiveReadyPending) + return; + m_receiveReadyPending = false; + target = m_started ? m_target : nullptr; + m_targetCallbackActive = target != nullptr; + } + if (target) + target->ClipboardReceiveReady(); + { + CSRWExclusiveLock lock(m_lock); + m_targetCallbackActive = false; + } +} + +void CNetworkClipboard::Impl::NotifyFailure() +{ + { + CSRWSharedLock lock(m_lock); + if (m_targetCallbackActive) + return; + } + + bool notify = false; + { + CSRWExclusiveLock lock(m_lock); + if (!m_targetCallbackActive && m_failed && !m_failureNotified && m_target) + { + m_failureNotified = true; + m_targetCallbackActive = true; + notify = true; + } + } + if (!notify) + return; + + CSRWExclusiveLock callbackLock(m_targetCallbackLock); + IClipboardTarget * target; + { + CSRWSharedLock lock(m_lock); + target = m_target; + } + if (target) + target->ClipboardFailed(); + { + CSRWExclusiveLock lock(m_lock); + m_targetCallbackActive = false; + } +} + +bool CNetworkClipboard::Impl::QueueClaimStatusNL( + const NetworkClipboardPeer& peer, uint64_t epoch, + LGNetClipboardClaimCode code, LGNetClipboardClaimFlags flags, + uint32_t leaseMs, uint32_t detail) +{ + LGNetClipboardClaimStatus status = {}; + status.claimEpoch = epoch; + status.serial = Next64(m_claimSerial); + status.status = code; + status.flags = flags; + status.leaseRemainingMs = leaseMs; + status.detail = detail; + + SendItem send; + if (!BuildSendNL(send, peer, LG_NET_SERVICE_CLIPBOARD, + LG_NET_CLIPBOARD_MESSAGE_CLAIM_RESULT, LG_NET_ENVELOPE_RESPONSE, epoch, + epoch, LG_NET_CLIPBOARD_CLAIM_STATUS_WIRE_SIZE, false) || + !lgNetClipboardClaimStatusEncode( + send.payload.data(), send.payload.size(), &status)) + return false; + + std::vector batch; + try + { + batch.emplace_back(std::move(send)); + } + catch (const std::bad_alloc&) + { + return false; + } + if (!QueueSendBatchNL(batch)) + return false; + m_claimSerial = status.serial; + return true; +} + +bool CNetworkClipboard::Impl::QueueControlNL( + uint16_t messageType, uint64_t offerID, std::vector& batch) +{ + if (!m_claimEpoch) + return false; + + LGNetClipboardControl control = {}; + control.ownerID = m_ownerID; + control.claimEpoch = m_claimEpoch; + control.serial = Next64(m_claimSerial); + control.offerID = offerID; + + SendItem send; + if (!BuildSendNL(send, m_owner, LG_NET_SERVICE_CLIPBOARD, messageType, 0, + m_claimEpoch, offerID, LG_NET_CLIPBOARD_CONTROL_WIRE_SIZE, false) || + !lgNetClipboardControlEncode( + send.payload.data(), send.payload.size(), &control)) + return false; + try + { + batch.emplace_back(std::move(send)); + } + catch (const std::bad_alloc&) + { + return false; + } + m_claimSerial = control.serial; + return true; +} + +bool CNetworkClipboard::Impl::QueueClipboardStatusNL( + const NetworkClipboardPeer& peer, uint64_t epoch, uint64_t requestID, + uint64_t offerID, LGNetClipboardStatusCode statusCode, uint32_t detail, + uint64_t processedLength, std::vector& batch) +{ + LGNetClipboardStatus status = {}; + status.requestID = requestID; + status.offerID = offerID; + status.status = statusCode; + status.detail = detail; + status.processedLength = processedLength; + + SendItem send; + if (!BuildSendNL(send, peer, LG_NET_SERVICE_CLIPBOARD, + LG_NET_CLIPBOARD_MESSAGE_STATUS, LG_NET_ENVELOPE_RESPONSE, epoch, + requestID, LG_NET_CLIPBOARD_STATUS_WIRE_SIZE, false) || + !lgNetClipboardStatusEncode( + send.payload.data(), send.payload.size(), &status)) + return false; + try + { + batch.emplace_back(std::move(send)); + } + catch (const std::bad_alloc&) + { + return false; + } + return true; +} + +bool CNetworkClipboard::Impl::QueueFileStatusNL( + const NetworkClipboardPeer& peer, uint64_t epoch, uint64_t requestID, + uint64_t offerID, uint64_t entryID, KVMFRClipboardFileError error, + uint64_t transferredLength, std::vector& batch) +{ + LGNetFileStatus status = {}; + status.requestID = requestID; + status.offerID = offerID; + status.entryID = entryID; + status.status = lgNetFileStatusFromKVMFRError(error); + status.detail = error; + status.transferredLength = transferredLength; + + SendItem send; + if (!BuildSendNL(send, peer, LG_NET_SERVICE_FILE, LG_NET_FILE_MESSAGE_STATUS, + LG_NET_ENVELOPE_RESPONSE, epoch, requestID, + LG_NET_FILE_STATUS_WIRE_SIZE, false) || + !lgNetFileStatusEncode(send.payload.data(), send.payload.size(), &status)) + return false; + try + { + batch.emplace_back(std::move(send)); + } + catch (const std::bad_alloc&) + { + return false; + } + return true; +} + +bool CNetworkClipboard::Impl::BuildCachedOfferNL( + std::vector& batch, const KVMFRClipboardMessage& record) +{ + if (!m_claimEpoch || !ValidOffer(record)) + return false; + + uint32_t count = 0; + for (uint32_t mask = record.token & KVMFR_CLIPBOARD_FORMAT_MASK_ALL; mask; + mask >>= 1) + count += mask & 1U; + if (!count || count > FORMAT_COUNT) + return false; + + if (record.token & KVMFR_CLIPBOARD_FORMAT_MASK_FILES) + { + LGNetFileOffer offer = {}; + offer.offerID = record.clipboardGeneration; + offer.totalBytes = 0; + offer.entryCount = 1; + offer.flags = LG_NET_FILE_OFFER_COPY | LG_NET_FILE_OFFER_RECURSIVE; + const size_t payloadLength = lgNetFileOfferSize(&offer); + if (!payloadLength || payloadLength > UINT32_MAX) + return false; + + SendItem send; + if (!BuildSendNL(send, m_owner, LG_NET_SERVICE_FILE, + LG_NET_FILE_MESSAGE_OFFER, 0, m_claimEpoch, offer.offerID, + static_cast(payloadLength), false) || + !lgNetFileOfferEncode(send.payload.data(), send.payload.size(), &offer)) + return false; + try + { + batch.emplace_back(std::move(send)); + } + catch (const std::bad_alloc&) + { + return false; + } + } + + uint32_t index = 0; + for (KVMFRClipboardFormat format = KVMFR_CLIPBOARD_FORMAT_TEXT; + format <= KVMFR_CLIPBOARD_FORMAT_FILES; ++format) + { + if (!(record.token & kvmfrClipboardFormatFlag(format))) + continue; + + const uint8_t * mime; + uint16_t mimeLength; + if (!lgNetClipboardKVMFRFormatMime(format, &mime, &mimeLength)) + return false; + + LGNetClipboardOffer offer = {}; + offer.offerID = record.clipboardGeneration; + offer.serial = record.clipboardGeneration; + offer.estimatedSize = 0; + offer.formatIndex = index++; + offer.formatCount = count; + offer.mimeLength = mimeLength; + offer.flags = LG_NET_CLIPBOARD_OFFER_LAZY; + offer.mime = mime; + const size_t payloadLength = lgNetClipboardOfferSize(&offer); + if (!payloadLength || payloadLength > UINT32_MAX) + return false; + + SendItem send; + if (!BuildSendNL(send, m_owner, LG_NET_SERVICE_CLIPBOARD, + LG_NET_CLIPBOARD_MESSAGE_OFFER, 0, m_claimEpoch, offer.offerID, + static_cast(payloadLength), false) || + !lgNetClipboardOfferEncode( + send.payload.data(), send.payload.size(), &offer)) + return false; + try + { + batch.emplace_back(std::move(send)); + } + catch (const std::bad_alloc&) + { + return false; + } + } + return true; +} + +bool CNetworkClipboard::Impl::ReplayCachedNL(std::vector& batch) +{ + if (!m_cachedValid || !m_claimEpoch) + return true; + if (m_cachedRecord.type == KVMFR_CLIPBOARD_MESSAGE_OFFER) + return BuildCachedOfferNL(batch, m_cachedRecord); + if (m_cachedRecord.type == KVMFR_CLIPBOARD_MESSAGE_CLEAR) + return QueueControlNL(LG_NET_CLIPBOARD_MESSAGE_CLEAR, + m_cachedRecord.clipboardGeneration, batch); + return false; +} + +void CNetworkClipboard::Impl::ResetTransfersNL() +{ + m_clipboardTransfers.clear(); + m_fileAcquisitions.clear(); + m_fileTransfers.clear(); +} + +void CNetworkClipboard::Impl::ClearRemoteTransfersNL(uint64_t offerID) +{ + m_clipboardTransfers.erase( + std::remove_if(m_clipboardTransfers.begin(), m_clipboardTransfers.end(), + [offerID](const ClipboardTransfer& transfer) + { + return transfer.direction == Direction::HELPER_REQUESTER && + transfer.offerID == offerID; + }), + m_clipboardTransfers.end()); + m_fileAcquisitions.erase( + std::remove_if(m_fileAcquisitions.begin(), m_fileAcquisitions.end(), + [offerID](const FileAcquisition& acquisition) + { + return acquisition.direction == Direction::HELPER_REQUESTER && + acquisition.offerID == offerID; + }), + m_fileAcquisitions.end()); + m_fileTransfers.erase( + std::remove_if(m_fileTransfers.begin(), m_fileTransfers.end(), + [offerID](const FileTransfer& transfer) + { + return transfer.direction == Direction::HELPER_REQUESTER && + transfer.offerID == offerID; + }), + m_fileTransfers.end()); +} + +bool CNetworkClipboard::Impl::PublishRemoteOfferNL() +{ + if (!m_remoteOffer.complete || m_remoteOffer.published || !m_started || + !m_stateKnown || !m_available || !m_generation) + return true; + + TargetItem item; + item.record.version = KVMFR_CLIPBOARD_VERSION; + item.record.type = KVMFR_CLIPBOARD_MESSAGE_OFFER; + item.record.clipboardGeneration = m_remoteOffer.offerID; + item.record.token = m_remoteOffer.formatMask; + if (!QueueTargetNL(std::move(item))) + return false; + m_remoteOffer.published = true; + return true; +} + +void CNetworkClipboard::Impl::DropOwnerNL(bool clearHelper) +{ + if (!m_claimEpoch) + return; + + if (clearHelper && m_remoteOffer.published && m_remoteOffer.offerID) + { + TargetItem item; + item.record.version = KVMFR_CLIPBOARD_VERSION; + item.record.type = KVMFR_CLIPBOARD_MESSAGE_CLEAR; + item.record.clipboardGeneration = m_remoteOffer.offerID; + QueueTargetNL(std::move(item)); + } + + if (clearHelper) + { + for (const ClipboardTransfer& transfer : m_clipboardTransfers) + { + TargetItem item; + item.record.version = KVMFR_CLIPBOARD_VERSION; + item.record.type = KVMFR_CLIPBOARD_MESSAGE_CANCEL; + item.record.clipboardGeneration = transfer.offerID; + item.record.transfer = transfer.localID; + item.record.format = transfer.format; + QueueTargetNL(std::move(item)); + } + + for (const FileTransfer& transfer : m_fileTransfers) + { + TargetItem item; + item.record.version = KVMFR_CLIPBOARD_VERSION; + item.record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL; + item.record.clipboardGeneration = transfer.offerID; + item.record.transfer = transfer.localID; + item.record.format = KVMFR_CLIPBOARD_FORMAT_FILES; + item.record.token = KVMFR_CLIPBOARD_FILE_ERROR_DISCONNECTED; + QueueTargetNL(std::move(item)); + } + + for (const FileAcquisition& acquisition : m_fileAcquisitions) + { + TargetItem item; + item.record.version = KVMFR_CLIPBOARD_VERSION; + item.record.clipboardGeneration = acquisition.offerID; + item.record.transfer = acquisition.localID; + item.record.format = KVMFR_CLIPBOARD_FORMAT_FILES; + if (acquisition.direction == Direction::REMOTE_REQUESTER && + acquisition.active) + { + item.record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE; + } + else if (acquisition.direction == Direction::HELPER_REQUESTER && + !acquisition.active) + { + item.record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED; + item.record.token = KVMFR_CLIPBOARD_FILE_ERROR_DISCONNECTED; + } + else + { + item.record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL; + item.record.token = KVMFR_CLIPBOARD_FILE_ERROR_DISCONNECTED; + } + QueueTargetNL(std::move(item)); + } + } + + m_sends.erase(std::remove_if(m_sends.begin(), m_sends.end(), + [this](const SendItem& item) + { + if (!SamePeer(item.peer, m_owner)) + return false; + m_sendBytes -= item.payload.size(); + return true; + }), + m_sends.end()); + ResetTransfersNL(); + m_remoteOffer.Clear(); + m_owner = {}; + m_ownerID = 0; + m_claimEpoch = 0; + m_claimSerial = 0; + m_remoteClaimSerial = 0; + m_remoteOfferSerial = 0; + m_claimDeadline = 0; + m_claimLeaseMs = 0; +} + +NetworkClipboardPacketResult CNetworkClipboard::Impl::HandleClaimNL( + PeerState& peer, const LGNetPacketView& packet, uint64_t nowMs) +{ + if (!DeliveryFlags( + packet.envelope, LG_NET_ENVELOPE_ACK_REQUIRED, false) || + packet.envelope.requestID != packet.envelope.componentEpoch) + return NetworkClipboardPacketResult::INVALID; + + LGNetClipboardClaim claim = {}; + if (lgNetClipboardClaimDecode(&claim, packet.payload, packet.payloadLength) != + LG_NET_PARSE_OK || + claim.claimEpoch != packet.envelope.componentEpoch) + return NetworkClipboardPacketResult::INVALID; + if (claim.flags != CLAIM_FLAGS) + { + peer.lastClaimEpoch = claim.claimEpoch; + return QueueClaimStatusNL(peer.peer, claim.claimEpoch, + LG_NET_CLIPBOARD_CLAIM_REJECTED, 0, 0, 3) ? + NetworkClipboardPacketResult::HANDLED : + NetworkClipboardPacketResult::INVALID; + } + + const bool exactOwner = + IsOwnerNL(peer.peer, claim.claimEpoch) && claim.ownerID == m_ownerID; + if (exactOwner) + { + m_claimDeadline = nowMs + claim.leaseMs; + m_claimLeaseMs = claim.leaseMs; + return QueueClaimStatusNL(peer.peer, claim.claimEpoch, + LG_NET_CLIPBOARD_CLAIM_ACCEPTED, CLAIM_FLAGS, claim.leaseMs, 0) ? + NetworkClipboardPacketResult::HANDLED : + NetworkClipboardPacketResult::INVALID; + } + + if (claim.claimEpoch <= peer.lastClaimEpoch) + { + return QueueClaimStatusNL(peer.peer, claim.claimEpoch, + LG_NET_CLIPBOARD_CLAIM_REJECTED, 0, 0, 1) ? + NetworkClipboardPacketResult::HANDLED : + NetworkClipboardPacketResult::INVALID; + } + + if (m_claimEpoch && !SamePeer(m_owner, peer.peer)) + { + peer.lastClaimEpoch = claim.claimEpoch; + return QueueClaimStatusNL(peer.peer, claim.claimEpoch, + LG_NET_CLIPBOARD_CLAIM_REJECTED, 0, 0, 2) ? + NetworkClipboardPacketResult::HANDLED : + NetworkClipboardPacketResult::INVALID; + } + + if (m_claimEpoch) + DropOwnerNL(true); + + m_owner = peer.peer; + m_ownerID = claim.ownerID; + m_claimEpoch = claim.claimEpoch; + m_claimSerial = 0; + m_remoteClaimSerial = 0; + m_remoteOfferSerial = 0; + m_claimDeadline = nowMs + claim.leaseMs; + m_claimLeaseMs = claim.leaseMs; + peer.lastClaimEpoch = claim.claimEpoch; + + if (!QueueClaimStatusNL(peer.peer, claim.claimEpoch, + LG_NET_CLIPBOARD_CLAIM_ACCEPTED, CLAIM_FLAGS, claim.leaseMs, 0)) + { + DropOwnerNL(false); + return NetworkClipboardPacketResult::INVALID; + } + + std::vector replay; + if (!ReplayCachedNL(replay) || !QueueSendBatchNL(replay)) + { + DropOwnerNL(false); + return NetworkClipboardPacketResult::INVALID; + } + return NetworkClipboardPacketResult::HANDLED; +} + +NetworkClipboardPacketResult CNetworkClipboard::Impl::HandleClipboardNL( + PeerState& peer, const LGNetPacketView& packet, bool bulk, uint64_t nowMs) +{ + const LGNetEnvelope& envelope = packet.envelope; + if (envelope.messageType == LG_NET_CLIPBOARD_MESSAGE_CLAIM) + { + if (bulk) + return NetworkClipboardPacketResult::INVALID; + return HandleClaimNL(peer, packet, nowMs); + } + + const bool transferData = + envelope.messageType == LG_NET_CLIPBOARD_MESSAGE_DATA_BEGIN || + envelope.messageType == LG_NET_CLIPBOARD_MESSAGE_DATA_CHUNK || + envelope.messageType == LG_NET_CLIPBOARD_MESSAGE_DATA_END; + if (!IsOwnerNL(peer.peer, envelope.componentEpoch)) + return NetworkClipboardPacketResult::IGNORED; + if ((bulk && !transferData) || + !DeliveryFlags(envelope, + envelope.messageType == LG_NET_CLIPBOARD_MESSAGE_REQUEST ? + LG_NET_ENVELOPE_ACK_REQUIRED : + envelope.messageType == LG_NET_CLIPBOARD_MESSAGE_STATUS ? + LG_NET_ENVELOPE_RESPONSE : + 0, + bulk)) + return NetworkClipboardPacketResult::INVALID; + + m_claimDeadline = nowMs + m_claimLeaseMs; + + switch (envelope.messageType) + { + case LG_NET_CLIPBOARD_MESSAGE_KEEPALIVE: + case LG_NET_CLIPBOARD_MESSAGE_RELEASE: + case LG_NET_CLIPBOARD_MESSAGE_CLEAR: + { + LGNetClipboardControl control = {}; + if (lgNetClipboardControlDecode( + &control, packet.payload, packet.payloadLength) != LG_NET_PARSE_OK || + control.ownerID != m_ownerID || control.claimEpoch != m_claimEpoch || + envelope.requestID != control.offerID) + return NetworkClipboardPacketResult::INVALID; + if (control.serial <= m_remoteClaimSerial) + return NetworkClipboardPacketResult::IGNORED; + m_remoteClaimSerial = control.serial; + + if (envelope.messageType == LG_NET_CLIPBOARD_MESSAGE_KEEPALIVE) + { + if (control.offerID) + return NetworkClipboardPacketResult::INVALID; + return NetworkClipboardPacketResult::HANDLED; + } + if (envelope.messageType == LG_NET_CLIPBOARD_MESSAGE_RELEASE) + { + if (control.offerID) + return NetworkClipboardPacketResult::INVALID; + DropOwnerNL(true); + return NetworkClipboardPacketResult::HANDLED; + } + + if (m_remoteOffer.offerID && + (!control.offerID || control.offerID == m_remoteOffer.offerID)) + { + if (m_remoteOffer.published) + { + TargetItem item; + item.record.version = KVMFR_CLIPBOARD_VERSION; + item.record.type = KVMFR_CLIPBOARD_MESSAGE_CLEAR; + item.record.clipboardGeneration = m_remoteOffer.offerID; + if (!QueueTargetNL(std::move(item))) + return NetworkClipboardPacketResult::INVALID; + } + ClearRemoteTransfersNL(m_remoteOffer.offerID); + m_remoteOffer.Clear(); + } + return NetworkClipboardPacketResult::HANDLED; + } + + case LG_NET_CLIPBOARD_MESSAGE_OFFER: + { + LGNetClipboardOffer offer = {}; + if (lgNetClipboardOfferDecode( + &offer, packet.payload, packet.payloadLength) != LG_NET_PARSE_OK || + envelope.requestID != offer.offerID || offer.formatCount > FORMAT_COUNT || + offer.estimatedSize || offer.flags != LG_NET_CLIPBOARD_OFFER_LAZY) + return NetworkClipboardPacketResult::INVALID; + + const KVMFRClipboardFormat format = + lgNetClipboardKVMFRFormatFromMime(offer.mime, offer.mimeLength); + if (!format) + return NetworkClipboardPacketResult::INVALID; + + const bool current = m_remoteOffer.offerID == offer.offerID && + m_remoteOffer.serial == offer.serial; + if (!current && offer.serial <= m_remoteOfferSerial) + return NetworkClipboardPacketResult::IGNORED; + if (!current) + { + const bool fileOffer = + m_remoteOffer.fileOffer && m_remoteOffer.offerID == offer.offerID; + if (m_remoteOffer.published) + { + TargetItem clear; + clear.record.version = KVMFR_CLIPBOARD_VERSION; + clear.record.type = KVMFR_CLIPBOARD_MESSAGE_CLEAR; + clear.record.clipboardGeneration = m_remoteOffer.offerID; + if (!QueueTargetNL(std::move(clear))) + return NetworkClipboardPacketResult::INVALID; + } + ClearRemoteTransfersNL(m_remoteOffer.offerID); + m_remoteOffer.Clear(); + m_remoteOffer.offerID = offer.offerID; + m_remoteOffer.serial = offer.serial; + m_remoteOffer.formatCount = offer.formatCount; + m_remoteOffer.fileOffer = fileOffer; + m_remoteOfferSerial = offer.serial; + } + + if (m_remoteOffer.formatCount != offer.formatCount || + offer.formatIndex >= FORMAT_COUNT) + return NetworkClipboardPacketResult::INVALID; + const uint32_t indexBit = UINT32_C(1) << offer.formatIndex; + const uint32_t formatBit = kvmfrClipboardFormatFlag(format); + if ((m_remoteOffer.received & indexBit) || + (m_remoteOffer.formatMask & formatBit)) + return NetworkClipboardPacketResult::INVALID; + + m_remoteOffer.formats[offer.formatIndex] = format; + m_remoteOffer.received |= indexBit; + m_remoteOffer.formatMask |= formatBit; + const uint32_t complete = (UINT32_C(1) << offer.formatCount) - 1U; + const bool needsFiles = + (m_remoteOffer.formatMask & KVMFR_CLIPBOARD_FORMAT_MASK_FILES) != 0; + m_remoteOffer.complete = m_remoteOffer.received == complete && + (!needsFiles || m_remoteOffer.fileOffer); + if (!PublishRemoteOfferNL()) + { + return NetworkClipboardPacketResult::INVALID; + } + return NetworkClipboardPacketResult::HANDLED; + } + + case LG_NET_CLIPBOARD_MESSAGE_REQUEST: + { + LGNetClipboardRequest request = {}; + if (lgNetClipboardRequestDecode( + &request, packet.payload, packet.payloadLength) != LG_NET_PARSE_OK || + request.requestID != envelope.requestID || request.offset || + FindClipboardWireNL(request.requestID)) + return NetworkClipboardPacketResult::INVALID; + + if (!m_started || !m_stateKnown || !m_available || !m_generation) + { + std::vector batch; + if (!QueueClipboardStatusNL(peer.peer, m_claimEpoch, request.requestID, + request.offerID, LG_NET_CLIPBOARD_STATUS_BUSY, 0, 0, batch) || + !QueueSendBatchNL(batch)) + return NetworkClipboardPacketResult::INVALID; + return NetworkClipboardPacketResult::HANDLED; + } + + if (m_clipboardTransfers.size() >= MAX_REQUESTS) + { + std::vector batch; + if (!QueueClipboardStatusNL(peer.peer, m_claimEpoch, request.requestID, + request.offerID, LG_NET_CLIPBOARD_STATUS_BUSY, 0, 0, batch) || + !QueueSendBatchNL(batch)) + return NetworkClipboardPacketResult::INVALID; + return NetworkClipboardPacketResult::HANDLED; + } + + if (!m_cachedValid || + m_cachedRecord.type != KVMFR_CLIPBOARD_MESSAGE_OFFER || + request.offerID != m_cachedRecord.clipboardGeneration) + { + std::vector batch; + if (!QueueClipboardStatusNL(peer.peer, m_claimEpoch, request.requestID, + request.offerID, LG_NET_CLIPBOARD_STATUS_NOT_FOUND, 0, 0, batch) || + !QueueSendBatchNL(batch)) + return NetworkClipboardPacketResult::INVALID; + return NetworkClipboardPacketResult::HANDLED; + } + + uint32_t index = 0; + KVMFRClipboardFormat format = KVMFR_CLIPBOARD_FORMAT_NONE; + for (KVMFRClipboardFormat candidate = KVMFR_CLIPBOARD_FORMAT_TEXT; + candidate <= KVMFR_CLIPBOARD_FORMAT_FILES; ++candidate) + { + if (!(m_cachedRecord.token & kvmfrClipboardFormatFlag(candidate))) + continue; + if (index++ == request.formatIndex) + { + format = candidate; + break; + } + } + if (!kvmfrClipboardRepresentationFormatValid(format)) + { + std::vector batch; + if (!QueueClipboardStatusNL(peer.peer, m_claimEpoch, request.requestID, + request.offerID, LG_NET_CLIPBOARD_STATUS_UNSUPPORTED, 0, 0, + batch) || + !QueueSendBatchNL(batch)) + return NetworkClipboardPacketResult::INVALID; + return NetworkClipboardPacketResult::HANDLED; + } + + const bool transferBulk = + SelectBulkNL(peer, LG_NET_SERVICE_CLIPBOARD, + LG_NET_CLIPBOARD_TRANSFER_WIRE_SIZE) && + ChunkLengthNL(peer, LG_NET_SERVICE_CLIPBOARD, + LG_NET_CLIPBOARD_CHUNK_HEADER_WIRE_SIZE, true) >= MIN_CHUNK_LENGTH; + const uint32_t routeChunk = ChunkLengthNL( + peer, LG_NET_SERVICE_CLIPBOARD, + LG_NET_CLIPBOARD_CHUNK_HEADER_WIRE_SIZE, transferBulk); + const uint32_t maxChunk = + (std::min)((std::min)(request.maxChunkLength, MAX_CHUNK_LENGTH), + routeChunk); + if (maxChunk < MIN_CHUNK_LENGTH) + { + std::vector batch; + if (!QueueClipboardStatusNL(peer.peer, m_claimEpoch, request.requestID, + request.offerID, LG_NET_CLIPBOARD_STATUS_UNSUPPORTED, 0, 0, + batch) || + !QueueSendBatchNL(batch)) + return NetworkClipboardPacketResult::INVALID; + return NetworkClipboardPacketResult::HANDLED; + } + + ClipboardTransfer transfer; + transfer.peer = peer.peer; + transfer.direction = Direction::REMOTE_REQUESTER; + transfer.claimEpoch = m_claimEpoch; + transfer.wireID = request.requestID; + transfer.localID = NextLocalNL(false); + transfer.offerID = request.offerID; + transfer.formatIndex = request.formatIndex; + transfer.maxChunk = maxChunk; + transfer.format = format; + transfer.bulk = transferBulk; + + TargetItem item; + item.record.version = KVMFR_CLIPBOARD_VERSION; + item.record.type = KVMFR_CLIPBOARD_MESSAGE_REQUEST; + item.record.clipboardGeneration = transfer.offerID; + item.record.transfer = transfer.localID; + item.record.format = transfer.format; + if (!QueueTargetNL(std::move(item))) + return NetworkClipboardPacketResult::INVALID; + try + { + m_clipboardTransfers.emplace_back(transfer); + } + catch (const std::bad_alloc&) + { + m_targets.pop_back(); + m_targetBytes -= sizeof(item.record); + return NetworkClipboardPacketResult::INVALID; + } + return NetworkClipboardPacketResult::HANDLED; + } + + case LG_NET_CLIPBOARD_MESSAGE_DATA_BEGIN: + { + LGNetClipboardTransfer wire = {}; + if (lgNetClipboardTransferDecode( + &wire, packet.payload, packet.payloadLength) != LG_NET_PARSE_OK || + wire.requestID != envelope.requestID || wire.processedLength) + return NetworkClipboardPacketResult::INVALID; + ClipboardTransfer * transfer = FindClipboardWireNL(wire.requestID); + if (!transfer) + return NetworkClipboardPacketResult::IGNORED; + if (transfer->direction != Direction::HELPER_REQUESTER || + transfer->offerID != wire.offerID || transfer->began) + return NetworkClipboardPacketResult::INVALID; + transfer->began = true; + transfer->bulk = bulk; + transfer->sizeHint = wire.totalLength; + transfer->offset = 0; + transfer->sequence = 0; + return NetworkClipboardPacketResult::HANDLED; + } + + case LG_NET_CLIPBOARD_MESSAGE_DATA_CHUNK: + { + LGNetClipboardChunk chunkValue = {}; + if (lgNetClipboardChunkDecode(&chunkValue, packet.payload, + packet.payloadLength) != LG_NET_PARSE_OK || + chunkValue.requestID != envelope.requestID || + chunkValue.dataLength > MAX_CHUNK_LENGTH) + return NetworkClipboardPacketResult::INVALID; + ClipboardTransfer * transfer = FindClipboardWireNL(chunkValue.requestID); + if (!transfer) + return NetworkClipboardPacketResult::IGNORED; + if (transfer->direction != Direction::HELPER_REQUESTER || + !transfer->began || transfer->bulk != bulk || + transfer->offerID != chunkValue.offerID || + transfer->offset != chunkValue.offset || + (transfer->sizeHint != KVMFR_CLIPBOARD_SIZE_UNKNOWN && + transfer->sizeHint != chunkValue.totalLength)) + return NetworkClipboardPacketResult::INVALID; + + TargetItem item; + item.record.version = KVMFR_CLIPBOARD_VERSION; + item.record.type = KVMFR_CLIPBOARD_MESSAGE_DATA; + item.record.clipboardGeneration = transfer->offerID; + item.record.transfer = transfer->localID; + item.record.offset = transfer->offset; + item.record.size = + transfer->offset ? KVMFR_CLIPBOARD_SIZE_UNKNOWN : transfer->sizeHint; + item.record.format = transfer->format; + item.record.sequence = transfer->sequence; + item.record.flags = transfer->offset ? 0 : + KVMFR_CLIPBOARD_FLAG_BEGIN; + item.record.length = chunkValue.dataLength; + try + { + item.data.assign( + chunkValue.data, chunkValue.data + chunkValue.dataLength); + } + catch (const std::bad_alloc&) + { + return NetworkClipboardPacketResult::INVALID; + } + if (!QueueTargetNL(std::move(item))) + return NetworkClipboardPacketResult::INVALID; + + transfer = FindClipboardWireNL(chunkValue.requestID); + if (!transfer) + return NetworkClipboardPacketResult::HANDLED; + transfer->offset += chunkValue.dataLength; + ++transfer->sequence; + return NetworkClipboardPacketResult::HANDLED; + } + + case LG_NET_CLIPBOARD_MESSAGE_DATA_END: + { + LGNetClipboardTransfer wire = {}; + if (lgNetClipboardTransferDecode( + &wire, packet.payload, packet.payloadLength) != LG_NET_PARSE_OK || + wire.requestID != envelope.requestID || + wire.processedLength != wire.totalLength) + return NetworkClipboardPacketResult::INVALID; + ClipboardTransfer * transfer = FindClipboardWireNL(wire.requestID); + if (!transfer) + return NetworkClipboardPacketResult::IGNORED; + if (transfer->direction != Direction::HELPER_REQUESTER || + !transfer->began || transfer->bulk != bulk || + transfer->offerID != wire.offerID || + transfer->offset != wire.processedLength || + (transfer->sizeHint != KVMFR_CLIPBOARD_SIZE_UNKNOWN && + transfer->sizeHint != wire.totalLength)) + return NetworkClipboardPacketResult::INVALID; + + TargetItem item; + item.record.version = KVMFR_CLIPBOARD_VERSION; + item.record.type = KVMFR_CLIPBOARD_MESSAGE_DATA; + item.record.clipboardGeneration = transfer->offerID; + item.record.transfer = transfer->localID; + item.record.offset = transfer->offset; + item.record.size = transfer->offset; + item.record.format = transfer->format; + item.record.sequence = transfer->sequence; + item.record.flags = KVMFR_CLIPBOARD_FLAG_END; + if (!transfer->offset) + item.record.flags |= KVMFR_CLIPBOARD_FLAG_BEGIN; + if (!QueueTargetNL(std::move(item))) + return NetworkClipboardPacketResult::INVALID; + m_clipboardTransfers.erase( + std::remove_if(m_clipboardTransfers.begin(), m_clipboardTransfers.end(), + [&wire](const ClipboardTransfer& value) + { return value.wireID == wire.requestID; }), + m_clipboardTransfers.end()); + return NetworkClipboardPacketResult::HANDLED; + } + + case LG_NET_CLIPBOARD_MESSAGE_DATA_READY: + { + LGNetClipboardTransfer wire = {}; + if (lgNetClipboardTransferDecode( + &wire, packet.payload, packet.payloadLength) != LG_NET_PARSE_OK || + wire.requestID != envelope.requestID || wire.processedLength) + return NetworkClipboardPacketResult::INVALID; + ClipboardTransfer * transfer = FindClipboardWireNL(wire.requestID); + if (!transfer) + return NetworkClipboardPacketResult::IGNORED; + if (transfer->direction != Direction::REMOTE_REQUESTER || + transfer->offerID != wire.offerID) + return NetworkClipboardPacketResult::INVALID; + if (m_sourceBlocked && m_sourceBlockedService == LG_NET_SERVICE_CLIPBOARD && + m_sourceBlockedTransfer == transfer->localID) + { + m_sourceBlocked = false; + m_sourceBlockedService = 0; + m_sourceBlockedTransfer = 0; + m_receiveReadyPending = true; + } + return NetworkClipboardPacketResult::HANDLED; + } + + case LG_NET_CLIPBOARD_MESSAGE_CANCEL: + case LG_NET_CLIPBOARD_MESSAGE_STATUS: + { + uint64_t requestID; + uint64_t offerID; + if (envelope.messageType == LG_NET_CLIPBOARD_MESSAGE_CANCEL) + { + LGNetClipboardTransfer wire = {}; + if (lgNetClipboardTransferDecode( + &wire, packet.payload, packet.payloadLength) != LG_NET_PARSE_OK || + wire.requestID != envelope.requestID) + return NetworkClipboardPacketResult::INVALID; + requestID = wire.requestID; + offerID = wire.offerID; + } + else + { + LGNetClipboardStatus status = {}; + if (lgNetClipboardStatusDecode( + &status, packet.payload, packet.payloadLength) != LG_NET_PARSE_OK || + status.requestID != envelope.requestID) + return NetworkClipboardPacketResult::INVALID; + if (status.status == LG_NET_CLIPBOARD_STATUS_OK || + status.status == LG_NET_CLIPBOARD_STATUS_BUSY) + return NetworkClipboardPacketResult::HANDLED; + requestID = status.requestID; + offerID = status.offerID; + } + + ClipboardTransfer * transfer = FindClipboardWireNL(requestID); + if (!transfer) + return NetworkClipboardPacketResult::IGNORED; + if (transfer->offerID != offerID) + return NetworkClipboardPacketResult::INVALID; + TargetItem item; + item.record.version = KVMFR_CLIPBOARD_VERSION; + item.record.type = KVMFR_CLIPBOARD_MESSAGE_CANCEL; + item.record.clipboardGeneration = transfer->offerID; + item.record.transfer = transfer->localID; + item.record.format = transfer->format; + if (!QueueTargetNL(std::move(item))) + return NetworkClipboardPacketResult::INVALID; + m_clipboardTransfers.erase( + std::remove_if(m_clipboardTransfers.begin(), m_clipboardTransfers.end(), + [requestID](const ClipboardTransfer& value) + { return value.wireID == requestID; }), + m_clipboardTransfers.end()); + return NetworkClipboardPacketResult::HANDLED; + } + + case LG_NET_CLIPBOARD_MESSAGE_CLAIM_RESULT: + default: + return NetworkClipboardPacketResult::INVALID; + } +} + +NetworkClipboardPacketResult CNetworkClipboard::Impl::HandleFileNL( + PeerState& peer, const LGNetPacketView& packet, bool bulk) +{ + const LGNetEnvelope& envelope = packet.envelope; + const bool transferData = + envelope.messageType == LG_NET_FILE_MESSAGE_DATA_BEGIN || + envelope.messageType == LG_NET_FILE_MESSAGE_DATA_CHUNK || + envelope.messageType == LG_NET_FILE_MESSAGE_DATA_END; + LGNetEnvelopeFlags expected = 0; + if (envelope.messageType == LG_NET_FILE_MESSAGE_ACQUIRE || + envelope.messageType == LG_NET_FILE_MESSAGE_REQUEST) + expected = LG_NET_ENVELOPE_ACK_REQUIRED; + else if (envelope.messageType == LG_NET_FILE_MESSAGE_ACQUIRED || + envelope.messageType == LG_NET_FILE_MESSAGE_STATUS) + expected = LG_NET_ENVELOPE_RESPONSE; + if (!IsOwnerNL(peer.peer, envelope.componentEpoch)) + return NetworkClipboardPacketResult::IGNORED; + if ((bulk && !transferData) || !DeliveryFlags(envelope, expected, bulk)) + return NetworkClipboardPacketResult::INVALID; + + switch (envelope.messageType) + { + case LG_NET_FILE_MESSAGE_OFFER: + { + LGNetFileOffer offer = {}; + if (lgNetFileOfferDecode(&offer, packet.payload, packet.payloadLength) != + LG_NET_PARSE_OK || + envelope.requestID != offer.offerID || offer.totalBytes || + offer.entryCount != 1 || offer.labelLength || + offer.flags != (LG_NET_FILE_OFFER_COPY | LG_NET_FILE_OFFER_RECURSIVE)) + return NetworkClipboardPacketResult::INVALID; + + if (m_remoteOffer.offerID && m_remoteOffer.offerID != offer.offerID) + { + if (m_remoteOffer.published) + { + TargetItem clear; + clear.record.version = KVMFR_CLIPBOARD_VERSION; + clear.record.type = KVMFR_CLIPBOARD_MESSAGE_CLEAR; + clear.record.clipboardGeneration = m_remoteOffer.offerID; + if (!QueueTargetNL(std::move(clear))) + return NetworkClipboardPacketResult::INVALID; + } + ClearRemoteTransfersNL(m_remoteOffer.offerID); + m_remoteOffer.Clear(); + } + m_remoteOffer.offerID = offer.offerID; + m_remoteOffer.fileOffer = true; + + const uint32_t complete = m_remoteOffer.formatCount ? + (UINT32_C(1) << m_remoteOffer.formatCount) - 1U : + 0; + m_remoteOffer.complete = complete && m_remoteOffer.received == complete && + (m_remoteOffer.formatMask & KVMFR_CLIPBOARD_FORMAT_MASK_FILES); + if (!PublishRemoteOfferNL()) + { + return NetworkClipboardPacketResult::INVALID; + } + return NetworkClipboardPacketResult::HANDLED; + } + + case LG_NET_FILE_MESSAGE_ACQUIRE: + { + LGNetFileLease lease = {}; + if (lgNetFileLeaseDecode(&lease, packet.payload, packet.payloadLength) != + LG_NET_PARSE_OK || + lease.clientID != envelope.requestID || lease.leaseEpoch || + lease.flags != LG_NET_FILE_LEASE_READ || + FindAcquisitionWireNL(lease.clientID)) + return NetworkClipboardPacketResult::INVALID; + + bool duplicate = false; + for (const FileAcquisition& acquisition : m_fileAcquisitions) + if (acquisition.direction == Direction::REMOTE_REQUESTER && + acquisition.offerID == lease.offerID) + { + duplicate = true; + break; + } + if (!m_started || !m_stateKnown || !m_available || !m_generation || + !m_cachedValid || m_cachedRecord.type != KVMFR_CLIPBOARD_MESSAGE_OFFER || + !(m_cachedRecord.token & KVMFR_CLIPBOARD_FORMAT_MASK_FILES) || + lease.offerID != m_cachedRecord.clipboardGeneration || duplicate || + m_fileAcquisitions.size() >= MAX_ACQUISITIONS) + { + std::vector batch; + if (!QueueFileStatusNL(peer.peer, m_claimEpoch, lease.clientID, + lease.offerID, 0, KVMFR_CLIPBOARD_FILE_ERROR_NOT_FOUND, 0, batch) || + !QueueSendBatchNL(batch)) + return NetworkClipboardPacketResult::INVALID; + return NetworkClipboardPacketResult::HANDLED; + } + + FileAcquisition acquisition; + acquisition.peer = peer.peer; + acquisition.direction = Direction::REMOTE_REQUESTER; + acquisition.claimEpoch = m_claimEpoch; + acquisition.offerID = lease.offerID; + acquisition.wireID = lease.clientID; + acquisition.localID = NextLocalNL(false); + + TargetItem item; + item.record.version = KVMFR_CLIPBOARD_VERSION; + item.record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE; + item.record.clipboardGeneration = acquisition.offerID; + item.record.transfer = acquisition.localID; + item.record.format = KVMFR_CLIPBOARD_FORMAT_FILES; + if (!QueueTargetNL(std::move(item))) + return NetworkClipboardPacketResult::INVALID; + try + { + m_fileAcquisitions.emplace_back(acquisition); + } + catch (const std::bad_alloc&) + { + m_targetBytes -= + sizeof(m_targets.back().record) + m_targets.back().data.size(); + m_targets.pop_back(); + return NetworkClipboardPacketResult::INVALID; + } + return NetworkClipboardPacketResult::HANDLED; + } + + case LG_NET_FILE_MESSAGE_ACQUIRED: + { + LGNetFileLease lease = {}; + if (lgNetFileLeaseDecode(&lease, packet.payload, packet.payloadLength) != + LG_NET_PARSE_OK || + lease.clientID != envelope.requestID || !lease.leaseEpoch || + lease.flags != + (LG_NET_FILE_LEASE_READ | LG_NET_FILE_LEASE_ACQUIRED)) + return NetworkClipboardPacketResult::INVALID; + FileAcquisition * acquisition = FindAcquisitionWireNL(lease.clientID); + if (!acquisition) + return NetworkClipboardPacketResult::IGNORED; + if (acquisition->direction != Direction::HELPER_REQUESTER || + acquisition->offerID != lease.offerID || acquisition->active) + return NetworkClipboardPacketResult::INVALID; + + TargetItem item; + item.record.version = KVMFR_CLIPBOARD_VERSION; + item.record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED; + item.record.clipboardGeneration = acquisition->offerID; + item.record.transfer = acquisition->localID; + item.record.format = KVMFR_CLIPBOARD_FORMAT_FILES; + item.record.token = KVMFR_CLIPBOARD_FILE_ERROR_NONE; + if (!QueueTargetNL(std::move(item))) + return NetworkClipboardPacketResult::INVALID; + acquisition = FindAcquisitionWireNL(lease.clientID); + if (acquisition) + { + acquisition->leaseEpoch = lease.leaseEpoch; + acquisition->active = true; + } + return NetworkClipboardPacketResult::HANDLED; + } + + case LG_NET_FILE_MESSAGE_RELEASE: + { + LGNetFileLease lease = {}; + if (lgNetFileLeaseDecode(&lease, packet.payload, packet.payloadLength) != + LG_NET_PARSE_OK || + lease.clientID != envelope.requestID || !lease.leaseEpoch || + lease.leaseMs || lease.flags != LG_NET_FILE_LEASE_READ) + return NetworkClipboardPacketResult::INVALID; + FileAcquisition * acquisition = FindAcquisitionWireNL(lease.clientID); + if (!acquisition) + return NetworkClipboardPacketResult::IGNORED; + if (acquisition->direction != Direction::REMOTE_REQUESTER || + acquisition->offerID != lease.offerID || + acquisition->leaseEpoch != lease.leaseEpoch || !acquisition->active) + return NetworkClipboardPacketResult::INVALID; + + const uint64_t localID = acquisition->localID; + TargetItem item; + item.record.version = KVMFR_CLIPBOARD_VERSION; + item.record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE; + item.record.clipboardGeneration = acquisition->offerID; + item.record.transfer = localID; + item.record.format = KVMFR_CLIPBOARD_FORMAT_FILES; + if (!QueueTargetNL(std::move(item))) + return NetworkClipboardPacketResult::INVALID; + const uint64_t offerID = acquisition->offerID; + m_fileAcquisitions.erase( + std::remove_if(m_fileAcquisitions.begin(), m_fileAcquisitions.end(), + [localID](const FileAcquisition& value) + { return value.localID == localID; }), + m_fileAcquisitions.end()); + m_fileTransfers.erase( + std::remove_if(m_fileTransfers.begin(), m_fileTransfers.end(), + [offerID](const FileTransfer& value) + { + return value.direction == Direction::REMOTE_REQUESTER && + value.offerID == offerID; + }), + m_fileTransfers.end()); + return NetworkClipboardPacketResult::HANDLED; + } + + case LG_NET_FILE_MESSAGE_REQUEST: + { + LGNetFileRequest request = {}; + if (lgNetFileRequestDecode( + &request, packet.payload, packet.payloadLength) != LG_NET_PARSE_OK || + request.requestID != envelope.requestID || + FindFileWireNL(request.requestID) || + m_fileTransfers.size() >= MAX_REQUESTS) + return NetworkClipboardPacketResult::INVALID; + + const KVMFRClipboardFileOperation operation = + lgNetFileKVMFROperationFromRequestFlags(request.flags); + uint64_t node; + if (!operation || !lgNetFileKVMFRNodeFromEntryID(request.entryID, &node) || + (operation == KVMFR_CLIPBOARD_FILE_OP_READ && + (node == KVMFR_CLIPBOARD_FILE_ROOT_NODE || + request.length > MAX_FILE_READ_LENGTH))) + return NetworkClipboardPacketResult::INVALID; + + if (!m_started || !m_stateKnown || !m_available || !m_generation) + { + std::vector batch; + if (!QueueFileStatusNL(peer.peer, m_claimEpoch, request.requestID, + request.offerID, request.entryID, + KVMFR_CLIPBOARD_FILE_ERROR_DISCONNECTED, 0, batch) || + !QueueSendBatchNL(batch)) + return NetworkClipboardPacketResult::INVALID; + return NetworkClipboardPacketResult::HANDLED; + } + + bool acquired = false; + for (const FileAcquisition& acquisition : m_fileAcquisitions) + if (acquisition.direction == Direction::REMOTE_REQUESTER && + acquisition.offerID == request.offerID && acquisition.active) + { + acquired = true; + break; + } + if (!acquired) + { + std::vector batch; + if (!QueueFileStatusNL(peer.peer, m_claimEpoch, request.requestID, + request.offerID, request.entryID, KVMFR_CLIPBOARD_FILE_ERROR_ACCESS, + 0, batch) || + !QueueSendBatchNL(batch)) + return NetworkClipboardPacketResult::INVALID; + return NetworkClipboardPacketResult::HANDLED; + } + + const bool transferBulk = + SelectBulkNL(peer, LG_NET_SERVICE_FILE, + LG_NET_FILE_TRANSFER_WIRE_SIZE) && + ChunkLengthNL(peer, LG_NET_SERVICE_FILE, + LG_NET_FILE_CHUNK_HEADER_WIRE_SIZE, true) >= MIN_CHUNK_LENGTH; + const uint32_t maxChunk = ChunkLengthNL( + peer, LG_NET_SERVICE_FILE, LG_NET_FILE_CHUNK_HEADER_WIRE_SIZE, + transferBulk); + if (maxChunk < MIN_CHUNK_LENGTH) + { + std::vector batch; + if (!QueueFileStatusNL(peer.peer, m_claimEpoch, request.requestID, + request.offerID, request.entryID, + KVMFR_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED, 0, batch) || + !QueueSendBatchNL(batch)) + return NetworkClipboardPacketResult::INVALID; + return NetworkClipboardPacketResult::HANDLED; + } + + FileTransfer transfer; + transfer.peer = peer.peer; + transfer.direction = Direction::REMOTE_REQUESTER; + transfer.claimEpoch = m_claimEpoch; + transfer.wireID = request.requestID; + transfer.localID = NextLocalNL(false); + transfer.offerID = request.offerID; + transfer.entryID = request.entryID; + transfer.requestOffset = request.offset; + transfer.length = request.length; + transfer.maxChunk = maxChunk; + transfer.operation = operation; + transfer.bulk = transferBulk; + + TargetItem item; + item.record.version = KVMFR_CLIPBOARD_VERSION; + item.record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST; + item.record.clipboardGeneration = transfer.offerID; + item.record.transfer = transfer.localID; + item.record.offset = transfer.requestOffset; + item.record.size = node; + item.record.format = KVMFR_CLIPBOARD_FORMAT_FILES; + item.record.flags = + transfer.operation == KVMFR_CLIPBOARD_FILE_OP_READ ? transfer.length : 0; + item.record.token = transfer.operation; + if (!QueueTargetNL(std::move(item))) + return NetworkClipboardPacketResult::INVALID; + try + { + m_fileTransfers.emplace_back(transfer); + } + catch (const std::bad_alloc&) + { + m_targetBytes -= + sizeof(m_targets.back().record) + m_targets.back().data.size(); + m_targets.pop_back(); + return NetworkClipboardPacketResult::INVALID; + } + return NetworkClipboardPacketResult::HANDLED; + } + + case LG_NET_FILE_MESSAGE_DATA_BEGIN: + { + LGNetFileTransfer wire = {}; + if (lgNetFileTransferDecode(&wire, packet.payload, packet.payloadLength) != + LG_NET_PARSE_OK || + wire.requestID != envelope.requestID || wire.processedLength || + wire.offset > wire.totalLength) + return NetworkClipboardPacketResult::INVALID; + FileTransfer * transfer = FindFileWireNL(wire.requestID); + if (!transfer) + return NetworkClipboardPacketResult::IGNORED; + if (transfer->direction != Direction::HELPER_REQUESTER || + transfer->offerID != wire.offerID || transfer->entryID != wire.entryID || + transfer->began || transfer->requestOffset != wire.offset || + (transfer->operation == KVMFR_CLIPBOARD_FILE_OP_READ && + (wire.totalLength - wire.offset > MAX_FILE_READ_LENGTH || + wire.totalLength - wire.offset > transfer->length))) + return NetworkClipboardPacketResult::INVALID; + transfer->began = true; + transfer->bulk = bulk; + transfer->sizeHint = wire.totalLength - wire.offset; + transfer->responseOffset = 0; + transfer->sequence = 0; + return NetworkClipboardPacketResult::HANDLED; + } + + case LG_NET_FILE_MESSAGE_DATA_CHUNK: + { + LGNetFileChunk chunkValue = {}; + if (lgNetFileChunkDecode(&chunkValue, packet.payload, + packet.payloadLength) != LG_NET_PARSE_OK || + chunkValue.requestID != envelope.requestID || + chunkValue.dataLength > MAX_CHUNK_LENGTH) + return NetworkClipboardPacketResult::INVALID; + FileTransfer * transfer = FindFileWireNL(chunkValue.requestID); + if (!transfer) + return NetworkClipboardPacketResult::IGNORED; + if (transfer->direction != Direction::HELPER_REQUESTER || + !transfer->began || transfer->bulk != bulk || + transfer->offerID != chunkValue.offerID || + transfer->entryID != chunkValue.entryID || + transfer->responseOffset != chunkValue.offset || + (transfer->sizeHint != KVMFR_CLIPBOARD_SIZE_UNKNOWN && + transfer->sizeHint != chunkValue.totalLength)) + return NetworkClipboardPacketResult::INVALID; + + TargetItem item; + item.record.version = KVMFR_CLIPBOARD_VERSION; + item.record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_DATA; + item.record.clipboardGeneration = transfer->offerID; + item.record.transfer = transfer->localID; + item.record.offset = transfer->responseOffset; + item.record.size = transfer->responseOffset ? + KVMFR_CLIPBOARD_SIZE_UNKNOWN : transfer->sizeHint; + item.record.format = KVMFR_CLIPBOARD_FORMAT_FILES; + item.record.sequence = transfer->sequence; + item.record.flags = + transfer->responseOffset ? 0 : KVMFR_CLIPBOARD_FLAG_BEGIN; + item.record.token = transfer->operation; + item.record.length = chunkValue.dataLength; + try + { + item.data.assign( + chunkValue.data, chunkValue.data + chunkValue.dataLength); + } + catch (const std::bad_alloc&) + { + return NetworkClipboardPacketResult::INVALID; + } + if (!QueueTargetNL(std::move(item))) + return NetworkClipboardPacketResult::INVALID; + + transfer = FindFileWireNL(chunkValue.requestID); + if (!transfer) + return NetworkClipboardPacketResult::HANDLED; + transfer->responseOffset += chunkValue.dataLength; + ++transfer->sequence; + return NetworkClipboardPacketResult::HANDLED; + } + + case LG_NET_FILE_MESSAGE_DATA_END: + { + LGNetFileTransfer wire = {}; + if (lgNetFileTransferDecode(&wire, packet.payload, packet.payloadLength) != + LG_NET_PARSE_OK || + wire.requestID != envelope.requestID || wire.offset > wire.totalLength || + wire.processedLength != wire.totalLength - wire.offset) + return NetworkClipboardPacketResult::INVALID; + FileTransfer * transfer = FindFileWireNL(wire.requestID); + if (!transfer) + return NetworkClipboardPacketResult::IGNORED; + if (transfer->direction != Direction::HELPER_REQUESTER || + !transfer->began || transfer->bulk != bulk || + transfer->offerID != wire.offerID || + transfer->entryID != wire.entryID || + transfer->requestOffset != wire.offset || + transfer->responseOffset != wire.processedLength || + (transfer->sizeHint != KVMFR_CLIPBOARD_SIZE_UNKNOWN && + transfer->sizeHint != wire.processedLength)) + return NetworkClipboardPacketResult::INVALID; + + TargetItem item; + item.record.version = KVMFR_CLIPBOARD_VERSION; + item.record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_DATA; + item.record.clipboardGeneration = transfer->offerID; + item.record.transfer = transfer->localID; + item.record.offset = transfer->responseOffset; + item.record.size = transfer->responseOffset; + item.record.format = KVMFR_CLIPBOARD_FORMAT_FILES; + item.record.sequence = transfer->sequence; + item.record.flags = KVMFR_CLIPBOARD_FLAG_END; + if (!transfer->responseOffset) + item.record.flags |= KVMFR_CLIPBOARD_FLAG_BEGIN; + item.record.token = transfer->operation; + if (!QueueTargetNL(std::move(item))) + return NetworkClipboardPacketResult::INVALID; + m_fileTransfers.erase( + std::remove_if(m_fileTransfers.begin(), m_fileTransfers.end(), + [&wire](const FileTransfer& value) + { return value.wireID == wire.requestID; }), + m_fileTransfers.end()); + return NetworkClipboardPacketResult::HANDLED; + } + + case LG_NET_FILE_MESSAGE_DATA_READY: + { + LGNetFileTransfer wire = {}; + if (lgNetFileTransferDecode(&wire, packet.payload, packet.payloadLength) != + LG_NET_PARSE_OK || + wire.requestID != envelope.requestID || wire.processedLength) + return NetworkClipboardPacketResult::INVALID; + FileTransfer * transfer = FindFileWireNL(wire.requestID); + if (!transfer) + return NetworkClipboardPacketResult::IGNORED; + if (transfer->direction != Direction::REMOTE_REQUESTER || + transfer->offerID != wire.offerID || transfer->entryID != wire.entryID) + return NetworkClipboardPacketResult::INVALID; + if (m_sourceBlocked && m_sourceBlockedService == LG_NET_SERVICE_FILE && + m_sourceBlockedTransfer == transfer->localID) + { + m_sourceBlocked = false; + m_sourceBlockedService = 0; + m_sourceBlockedTransfer = 0; + m_receiveReadyPending = true; + } + return NetworkClipboardPacketResult::HANDLED; + } + + case LG_NET_FILE_MESSAGE_CANCEL: + { + LGNetFileTransfer wire = {}; + if (lgNetFileTransferDecode(&wire, packet.payload, packet.payloadLength) != + LG_NET_PARSE_OK || + wire.requestID != envelope.requestID) + return NetworkClipboardPacketResult::INVALID; + FileTransfer * transfer = FindFileWireNL(wire.requestID); + if (!transfer) + return NetworkClipboardPacketResult::IGNORED; + if (transfer->offerID != wire.offerID || + transfer->entryID != wire.entryID) + return NetworkClipboardPacketResult::INVALID; + const uint64_t localID = transfer->localID; + TargetItem item; + item.record.version = KVMFR_CLIPBOARD_VERSION; + item.record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL; + item.record.clipboardGeneration = transfer->offerID; + item.record.transfer = localID; + item.record.format = KVMFR_CLIPBOARD_FORMAT_FILES; + item.record.token = KVMFR_CLIPBOARD_FILE_ERROR_CANCELLED; + if (!QueueTargetNL(std::move(item))) + return NetworkClipboardPacketResult::INVALID; + m_fileTransfers.erase( + std::remove_if(m_fileTransfers.begin(), m_fileTransfers.end(), + [localID](const FileTransfer& value) + { return value.localID == localID; }), + m_fileTransfers.end()); + return NetworkClipboardPacketResult::HANDLED; + } + + case LG_NET_FILE_MESSAGE_STATUS: + { + LGNetFileStatus status = {}; + if (lgNetFileStatusDecode(&status, packet.payload, packet.payloadLength) != + LG_NET_PARSE_OK || + status.requestID != envelope.requestID) + return NetworkClipboardPacketResult::INVALID; + if (status.status == LG_NET_FILE_STATUS_OK) + return NetworkClipboardPacketResult::HANDLED; + const KVMFRClipboardFileError error = + lgNetFileKVMFRErrorFromStatus(status.status, status.detail); + + if (!status.entryID) + { + FileAcquisition * acquisition = FindAcquisitionWireNL(status.requestID); + if (!acquisition) + return NetworkClipboardPacketResult::IGNORED; + if (acquisition->direction != Direction::HELPER_REQUESTER || + acquisition->offerID != status.offerID) + return NetworkClipboardPacketResult::INVALID; + const uint64_t localID = acquisition->localID; + TargetItem item; + item.record.version = KVMFR_CLIPBOARD_VERSION; + item.record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED; + item.record.clipboardGeneration = acquisition->offerID; + item.record.transfer = localID; + item.record.format = KVMFR_CLIPBOARD_FORMAT_FILES; + item.record.token = error; + if (!QueueTargetNL(std::move(item))) + return NetworkClipboardPacketResult::INVALID; + m_fileAcquisitions.erase( + std::remove_if(m_fileAcquisitions.begin(), m_fileAcquisitions.end(), + [localID](const FileAcquisition& value) + { return value.localID == localID; }), + m_fileAcquisitions.end()); + return NetworkClipboardPacketResult::HANDLED; + } + + FileTransfer * transfer = FindFileWireNL(status.requestID); + if (!transfer) + return NetworkClipboardPacketResult::IGNORED; + if (transfer->offerID != status.offerID || + transfer->entryID != status.entryID) + return NetworkClipboardPacketResult::INVALID; + const uint64_t localID = transfer->localID; + TargetItem item; + item.record.version = KVMFR_CLIPBOARD_VERSION; + item.record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL; + item.record.clipboardGeneration = transfer->offerID; + item.record.transfer = localID; + item.record.format = KVMFR_CLIPBOARD_FORMAT_FILES; + item.record.token = error; + if (!QueueTargetNL(std::move(item))) + return NetworkClipboardPacketResult::INVALID; + m_fileTransfers.erase( + std::remove_if(m_fileTransfers.begin(), m_fileTransfers.end(), + [localID](const FileTransfer& value) + { return value.localID == localID; }), + m_fileTransfers.end()); + return NetworkClipboardPacketResult::HANDLED; + } + + case LG_NET_FILE_MESSAGE_ENTRY: + default: + return NetworkClipboardPacketResult::INVALID; + } +} + +ClipboardChannelResult CNetworkClipboard::Impl::HandleHelperClipboardNL( + const KVMFRClipboardMessage& record, const uint8_t * data) +{ + if (record.type == KVMFR_CLIPBOARD_MESSAGE_OFFER || + record.type == KVMFR_CLIPBOARD_MESSAGE_CLEAR) + { + if ((record.type == KVMFR_CLIPBOARD_MESSAGE_OFFER && !ValidOffer(record)) || + (record.type == KVMFR_CLIPBOARD_MESSAGE_CLEAR && !ValidClear(record))) + return ClipboardChannelResult::FAILED; + + std::vector batch; + if (m_claimEpoch) + { + bool built = true; + for (const ClipboardTransfer& transfer : m_clipboardTransfers) + if (transfer.direction == Direction::REMOTE_REQUESTER) + built = built && QueueClipboardStatusNL( + transfer.peer, transfer.claimEpoch, transfer.wireID, + transfer.offerID, LG_NET_CLIPBOARD_STATUS_CANCELLED, 0, + transfer.offset, batch); + for (const FileTransfer& transfer : m_fileTransfers) + if (transfer.direction == Direction::REMOTE_REQUESTER) + built = built && QueueFileStatusNL( + transfer.peer, transfer.claimEpoch, transfer.wireID, + transfer.offerID, transfer.entryID, + KVMFR_CLIPBOARD_FILE_ERROR_CANCELLED, transfer.responseOffset, + batch); + for (const FileAcquisition& acquisition : m_fileAcquisitions) + if (acquisition.direction == Direction::REMOTE_REQUESTER) + built = built && QueueFileStatusNL( + acquisition.peer, acquisition.claimEpoch, acquisition.wireID, + acquisition.offerID, 0, KVMFR_CLIPBOARD_FILE_ERROR_CANCELLED, 0, + batch); + + built = built && (record.type == KVMFR_CLIPBOARD_MESSAGE_OFFER ? + BuildCachedOfferNL(batch, record) : + QueueControlNL(LG_NET_CLIPBOARD_MESSAGE_CLEAR, + record.clipboardGeneration, batch)); + if (!built || !QueueSendBatchNL(batch)) + { + m_sourceBlocked = true; + m_sourceBlockedService = LG_NET_SERVICE_CLIPBOARD; + m_sourceBlockedTransfer = 0; + return ClipboardChannelResult::BUSY; + } + } + + m_cachedRecord = record; + m_cachedValid = true; + m_clipboardTransfers.erase( + std::remove_if(m_clipboardTransfers.begin(), m_clipboardTransfers.end(), + [](const ClipboardTransfer& transfer) + { return transfer.direction == Direction::REMOTE_REQUESTER; }), + m_clipboardTransfers.end()); + m_fileAcquisitions.erase( + std::remove_if(m_fileAcquisitions.begin(), m_fileAcquisitions.end(), + [](const FileAcquisition& acquisition) + { return acquisition.direction == Direction::REMOTE_REQUESTER; }), + m_fileAcquisitions.end()); + m_fileTransfers.erase( + std::remove_if(m_fileTransfers.begin(), m_fileTransfers.end(), + [](const FileTransfer& transfer) + { return transfer.direction == Direction::REMOTE_REQUESTER; }), + m_fileTransfers.end()); + return ClipboardChannelResult::ACCEPTED; + } + + if (!m_claimEpoch) + { + if (record.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST && + ValidRequest(record, true)) + { + TargetItem item; + item.record.version = KVMFR_CLIPBOARD_VERSION; + item.record.type = KVMFR_CLIPBOARD_MESSAGE_CANCEL; + item.record.clipboardGeneration = record.clipboardGeneration; + item.record.transfer = record.transfer; + item.record.format = record.format; + QueueTargetNL(std::move(item)); + return ClipboardChannelResult::ACCEPTED; + } + return record.type == KVMFR_CLIPBOARD_MESSAGE_CANCEL && + ValidCancel(record) ? + ClipboardChannelResult::ACCEPTED : + ClipboardChannelResult::FAILED; + } + + if (record.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST) + { + if (!ValidRequest(record, true) || !m_remoteOffer.published || + record.clipboardGeneration != m_remoteOffer.offerID || + FindClipboardLocalNL(record.transfer) || + m_clipboardTransfers.size() >= MAX_REQUESTS) + return ClipboardChannelResult::FAILED; + + uint32_t formatIndex = UINT32_MAX; + for (uint32_t index = 0; index < m_remoteOffer.formatCount; ++index) + if (m_remoteOffer.formats[index] == record.format) + { + formatIndex = index; + break; + } + if (formatIndex == UINT32_MAX || + !kvmfrClipboardRepresentationFormatValid(record.format)) + return ClipboardChannelResult::FAILED; + + ClipboardTransfer transfer; + transfer.peer = m_owner; + transfer.direction = Direction::HELPER_REQUESTER; + transfer.claimEpoch = m_claimEpoch; + transfer.wireID = NextWireNL(); + transfer.localID = record.transfer; + transfer.offerID = record.clipboardGeneration; + transfer.formatIndex = formatIndex; + transfer.format = record.format; + const PeerState * peer = FindPeerNL(transfer.peer); + if (!peer) + return ClipboardChannelResult::FAILED; + transfer.bulk = SelectBulkNL(*peer, LG_NET_SERVICE_CLIPBOARD, + LG_NET_CLIPBOARD_TRANSFER_WIRE_SIZE) && + ChunkLengthNL(*peer, LG_NET_SERVICE_CLIPBOARD, + LG_NET_CLIPBOARD_CHUNK_HEADER_WIRE_SIZE, true) >= MIN_CHUNK_LENGTH; + transfer.maxChunk = ChunkLengthNL(*peer, LG_NET_SERVICE_CLIPBOARD, + LG_NET_CLIPBOARD_CHUNK_HEADER_WIRE_SIZE, transfer.bulk); + if (transfer.maxChunk < MIN_CHUNK_LENGTH) + return ClipboardChannelResult::FAILED; + + LGNetClipboardRequest request = {}; + request.offerID = transfer.offerID; + request.requestID = transfer.wireID; + request.formatIndex = transfer.formatIndex; + request.maxChunkLength = transfer.maxChunk; + request.offset = 0; + SendItem send; + if (!BuildSendNL(send, transfer.peer, LG_NET_SERVICE_CLIPBOARD, + LG_NET_CLIPBOARD_MESSAGE_REQUEST, LG_NET_ENVELOPE_ACK_REQUIRED, + transfer.claimEpoch, transfer.wireID, + LG_NET_CLIPBOARD_REQUEST_WIRE_SIZE, false) || + !lgNetClipboardRequestEncode( + send.payload.data(), send.payload.size(), &request)) + return ClipboardChannelResult::FAILED; + + std::vector batch; + try + { + batch.emplace_back(std::move(send)); + } + catch (const std::bad_alloc&) + { + return ClipboardChannelResult::FAILED; + } + if (!QueueSendBatchNL(batch)) + { + m_sourceBlocked = true; + m_sourceBlockedService = LG_NET_SERVICE_CLIPBOARD; + m_sourceBlockedTransfer = record.transfer; + return ClipboardChannelResult::BUSY; + } + try + { + m_clipboardTransfers.emplace_back(transfer); + } + catch (const std::bad_alloc&) + { + m_failed = true; + return ClipboardChannelResult::FAILED; + } + return ClipboardChannelResult::ACCEPTED; + } + + if (record.type == KVMFR_CLIPBOARD_MESSAGE_DATA) + { + if (!ValidData(record) || (record.length && !data)) + return ClipboardChannelResult::FAILED; + ClipboardTransfer * transfer = FindClipboardLocalNL(record.transfer); + if (!transfer || transfer->direction != Direction::REMOTE_REQUESTER || + transfer->offerID != record.clipboardGeneration || + transfer->format != record.format || transfer->offset != record.offset || + transfer->sequence != record.sequence) + return ClipboardChannelResult::FAILED; + if ((!transfer->began && !(record.flags & KVMFR_CLIPBOARD_FLAG_BEGIN)) || + (transfer->began && (record.flags & KVMFR_CLIPBOARD_FLAG_BEGIN))) + return ClipboardChannelResult::FAILED; + + const uint64_t sizeHint = + transfer->began ? transfer->sizeHint : record.size; + const uint64_t end = record.offset + record.length; + if ((sizeHint != KVMFR_CLIPBOARD_SIZE_UNKNOWN && end > sizeHint) || + ((record.flags & KVMFR_CLIPBOARD_FLAG_END) && record.size != end)) + return ClipboardChannelResult::FAILED; + + std::vector batch; + if (!transfer->began) + { + LGNetClipboardTransfer begin = {}; + begin.requestID = transfer->wireID; + begin.offerID = transfer->offerID; + begin.totalLength = record.size; + begin.processedLength = 0; + SendItem send; + if (!BuildSendForDeliveryNL(send, transfer->peer, + LG_NET_SERVICE_CLIPBOARD, + LG_NET_CLIPBOARD_MESSAGE_DATA_BEGIN, 0, transfer->claimEpoch, + transfer->wireID, LG_NET_CLIPBOARD_TRANSFER_WIRE_SIZE, + transfer->bulk) || + !lgNetClipboardTransferEncode( + send.payload.data(), send.payload.size(), &begin)) + return ClipboardChannelResult::FAILED; + try + { + batch.emplace_back(std::move(send)); + } + catch (const std::bad_alloc&) + { + return ClipboardChannelResult::FAILED; + } + } + + uint32_t consumed = 0; + const uint64_t wireLength = + (record.flags & KVMFR_CLIPBOARD_FLAG_END) ? end : sizeHint; + while (consumed < record.length) + { + const uint32_t length = + (std::min)(transfer->maxChunk, record.length - consumed); + LGNetClipboardChunk chunk = {}; + chunk.requestID = transfer->wireID; + chunk.offerID = transfer->offerID; + chunk.offset = record.offset + consumed; + chunk.totalLength = wireLength; + chunk.dataLength = length; + chunk.flags = chunk.offset ? 0 : + LG_NET_CLIPBOARD_CHUNK_FIRST; + if ((record.flags & KVMFR_CLIPBOARD_FLAG_END) && + consumed + length == record.length) + chunk.flags |= LG_NET_CLIPBOARD_CHUNK_FINAL; + chunk.data = data + consumed; + const size_t payloadLength = lgNetClipboardChunkSize(&chunk); + if (!payloadLength || payloadLength > UINT32_MAX) + return ClipboardChannelResult::FAILED; + SendItem send; + if (!BuildSendForDeliveryNL(send, transfer->peer, + LG_NET_SERVICE_CLIPBOARD, + LG_NET_CLIPBOARD_MESSAGE_DATA_CHUNK, 0, transfer->claimEpoch, + transfer->wireID, static_cast(payloadLength), + transfer->bulk) || + !lgNetClipboardChunkEncode( + send.payload.data(), send.payload.size(), &chunk)) + return ClipboardChannelResult::FAILED; + try + { + batch.emplace_back(std::move(send)); + } + catch (const std::bad_alloc&) + { + return ClipboardChannelResult::FAILED; + } + consumed += length; + } + + if (record.flags & KVMFR_CLIPBOARD_FLAG_END) + { + LGNetClipboardTransfer finish = {}; + finish.requestID = transfer->wireID; + finish.offerID = transfer->offerID; + finish.totalLength = end; + finish.processedLength = end; + SendItem send; + if (!BuildSendForDeliveryNL(send, transfer->peer, + LG_NET_SERVICE_CLIPBOARD, + LG_NET_CLIPBOARD_MESSAGE_DATA_END, 0, transfer->claimEpoch, + transfer->wireID, LG_NET_CLIPBOARD_TRANSFER_WIRE_SIZE, + transfer->bulk) || + !lgNetClipboardTransferEncode( + send.payload.data(), send.payload.size(), &finish)) + return ClipboardChannelResult::FAILED; + try + { + batch.emplace_back(std::move(send)); + } + catch (const std::bad_alloc&) + { + return ClipboardChannelResult::FAILED; + } + } + + if (!QueueSendBatchNL(batch)) + { + m_sourceBlocked = true; + m_sourceBlockedService = LG_NET_SERVICE_CLIPBOARD; + m_sourceBlockedTransfer = record.transfer; + return ClipboardChannelResult::BUSY; + } + + transfer = FindClipboardLocalNL(record.transfer); + if (!transfer) + return ClipboardChannelResult::ACCEPTED; + if (!transfer->began) + { + transfer->began = true; + transfer->sizeHint = record.size; + } + transfer->offset += record.length; + ++transfer->sequence; + if (record.flags & KVMFR_CLIPBOARD_FLAG_END) + m_clipboardTransfers.erase( + std::remove_if(m_clipboardTransfers.begin(), m_clipboardTransfers.end(), + [&record](const ClipboardTransfer& value) + { return value.localID == record.transfer; }), + m_clipboardTransfers.end()); + return ClipboardChannelResult::ACCEPTED; + } + + if (record.type == KVMFR_CLIPBOARD_MESSAGE_CANCEL) + { + if (!ValidCancel(record)) + return ClipboardChannelResult::FAILED; + ClipboardTransfer * transfer = FindClipboardLocalNL(record.transfer); + if (!transfer) + return ClipboardChannelResult::ACCEPTED; + + LGNetClipboardTransfer wire = {}; + wire.requestID = transfer->wireID; + wire.offerID = transfer->offerID; + wire.totalLength = transfer->sizeHint; + wire.processedLength = transfer->offset; + SendItem send; + if (!BuildSendNL(send, transfer->peer, LG_NET_SERVICE_CLIPBOARD, + LG_NET_CLIPBOARD_MESSAGE_CANCEL, 0, transfer->claimEpoch, + transfer->wireID, LG_NET_CLIPBOARD_TRANSFER_WIRE_SIZE, false) || + !lgNetClipboardTransferEncode( + send.payload.data(), send.payload.size(), &wire)) + return ClipboardChannelResult::FAILED; + std::vector batch; + try + { + batch.emplace_back(std::move(send)); + } + catch (const std::bad_alloc&) + { + return ClipboardChannelResult::FAILED; + } + if (!QueueSendBatchNL(batch)) + { + m_sourceBlocked = true; + m_sourceBlockedService = LG_NET_SERVICE_CLIPBOARD; + m_sourceBlockedTransfer = record.transfer; + return ClipboardChannelResult::BUSY; + } + m_clipboardTransfers.erase( + std::remove_if(m_clipboardTransfers.begin(), m_clipboardTransfers.end(), + [&record](const ClipboardTransfer& value) + { return value.localID == record.transfer; }), + m_clipboardTransfers.end()); + return ClipboardChannelResult::ACCEPTED; + } + + return ClipboardChannelResult::FAILED; +} + +ClipboardChannelResult CNetworkClipboard::Impl::HandleHelperFileNL( + const KVMFRClipboardMessage& record, const uint8_t * data) +{ + if (!kvmfrClipboardFileMessageValid(&record) || (record.length && !data)) + return ClipboardChannelResult::FAILED; + + if (!m_claimEpoch) + { + if (record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE || + record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST) + { + TargetItem item; + item.record.version = KVMFR_CLIPBOARD_VERSION; + item.record.clipboardGeneration = record.clipboardGeneration; + item.record.transfer = record.transfer; + item.record.format = KVMFR_CLIPBOARD_FORMAT_FILES; + item.record.type = record.type == + KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE ? + KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED : + KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL; + item.record.token = + KVMFR_CLIPBOARD_FILE_ERROR_DISCONNECTED; + QueueTargetNL(std::move(item)); + return ClipboardChannelResult::ACCEPTED; + } + return ClipboardChannelResult::ACCEPTED; + } + + switch (record.type) + { + case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE: + { + bool duplicate = false; + for (const FileAcquisition& acquisition : m_fileAcquisitions) + if (acquisition.direction == Direction::HELPER_REQUESTER && + acquisition.offerID == record.clipboardGeneration) + { + duplicate = true; + break; + } + if (!kvmfrClipboardTransferFromHelper(record.transfer) || + !m_remoteOffer.published || !m_remoteOffer.fileOffer || + record.clipboardGeneration != m_remoteOffer.offerID || + FindAcquisitionLocalNL(record.transfer) || duplicate || + m_fileAcquisitions.size() >= MAX_ACQUISITIONS) + return ClipboardChannelResult::FAILED; + + FileAcquisition acquisition; + acquisition.peer = m_owner; + acquisition.direction = Direction::HELPER_REQUESTER; + acquisition.claimEpoch = m_claimEpoch; + acquisition.offerID = record.clipboardGeneration; + acquisition.wireID = NextWireNL(); + acquisition.localID = record.transfer; + + LGNetFileLease lease = {}; + lease.clientID = acquisition.wireID; + lease.offerID = acquisition.offerID; + lease.leaseEpoch = 0; + lease.leaseMs = LG_NET_FILE_MAX_LEASE_MS; + lease.flags = LG_NET_FILE_LEASE_READ; + SendItem send; + if (!BuildSendNL(send, acquisition.peer, LG_NET_SERVICE_FILE, + LG_NET_FILE_MESSAGE_ACQUIRE, LG_NET_ENVELOPE_ACK_REQUIRED, + acquisition.claimEpoch, acquisition.wireID, + LG_NET_FILE_LEASE_WIRE_SIZE, false) || + !lgNetFileLeaseEncode(send.payload.data(), send.payload.size(), &lease)) + return ClipboardChannelResult::FAILED; + + std::vector batch; + try + { + batch.emplace_back(std::move(send)); + } + catch (const std::bad_alloc&) + { + return ClipboardChannelResult::FAILED; + } + if (!QueueSendBatchNL(batch)) + { + m_sourceBlocked = true; + m_sourceBlockedService = LG_NET_SERVICE_FILE; + m_sourceBlockedTransfer = record.transfer; + return ClipboardChannelResult::BUSY; + } + try + { + m_fileAcquisitions.emplace_back(acquisition); + } + catch (const std::bad_alloc&) + { + m_failed = true; + return ClipboardChannelResult::FAILED; + } + return ClipboardChannelResult::ACCEPTED; + } + + case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED: + { + if (!kvmfrClipboardTransferFromClient(record.transfer)) + return ClipboardChannelResult::FAILED; + FileAcquisition * acquisition = FindAcquisitionLocalNL(record.transfer); + if (!acquisition || acquisition->direction != Direction::REMOTE_REQUESTER || + acquisition->offerID != record.clipboardGeneration || acquisition->active) + return ClipboardChannelResult::FAILED; + + std::vector batch; + uint64_t leaseEpoch = 0; + if (record.token == KVMFR_CLIPBOARD_FILE_ERROR_NONE) + { + leaseEpoch = NextEpochNL(); + LGNetFileLease lease = {}; + lease.clientID = acquisition->wireID; + lease.offerID = acquisition->offerID; + lease.leaseEpoch = leaseEpoch; + lease.leaseMs = LG_NET_FILE_MAX_LEASE_MS; + lease.flags = LG_NET_FILE_LEASE_READ | + LG_NET_FILE_LEASE_ACQUIRED; + SendItem send; + if (!BuildSendNL(send, acquisition->peer, LG_NET_SERVICE_FILE, + LG_NET_FILE_MESSAGE_ACQUIRED, LG_NET_ENVELOPE_RESPONSE, + acquisition->claimEpoch, acquisition->wireID, + LG_NET_FILE_LEASE_WIRE_SIZE, false) || + !lgNetFileLeaseEncode(send.payload.data(), send.payload.size(), &lease)) + return ClipboardChannelResult::FAILED; + try + { + batch.emplace_back(std::move(send)); + } + catch (const std::bad_alloc&) + { + return ClipboardChannelResult::FAILED; + } + } + else if (!QueueFileStatusNL(acquisition->peer, acquisition->claimEpoch, + acquisition->wireID, acquisition->offerID, 0, + static_cast(record.token), 0, batch)) + return ClipboardChannelResult::FAILED; + + if (!QueueSendBatchNL(batch)) + { + m_sourceBlocked = true; + m_sourceBlockedService = LG_NET_SERVICE_FILE; + m_sourceBlockedTransfer = record.transfer; + return ClipboardChannelResult::BUSY; + } + + acquisition = FindAcquisitionLocalNL(record.transfer); + if (!acquisition) + return ClipboardChannelResult::ACCEPTED; + if (record.token == KVMFR_CLIPBOARD_FILE_ERROR_NONE) + { + acquisition->leaseEpoch = leaseEpoch; + acquisition->active = true; + } + else + m_fileAcquisitions.erase( + std::remove_if(m_fileAcquisitions.begin(), m_fileAcquisitions.end(), + [&record](const FileAcquisition& value) + { return value.localID == record.transfer; }), + m_fileAcquisitions.end()); + return ClipboardChannelResult::ACCEPTED; + } + + case KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE: + { + if (!kvmfrClipboardTransferFromHelper(record.transfer)) + return ClipboardChannelResult::FAILED; + FileAcquisition * acquisition = FindAcquisitionLocalNL(record.transfer); + if (!acquisition || acquisition->direction != Direction::HELPER_REQUESTER || + acquisition->offerID != record.clipboardGeneration || + !acquisition->active) + return ClipboardChannelResult::ACCEPTED; + + LGNetFileLease lease = {}; + lease.clientID = acquisition->wireID; + lease.offerID = acquisition->offerID; + lease.leaseEpoch = acquisition->leaseEpoch; + lease.leaseMs = 0; + lease.flags = LG_NET_FILE_LEASE_READ; + SendItem send; + if (!BuildSendNL(send, acquisition->peer, LG_NET_SERVICE_FILE, + LG_NET_FILE_MESSAGE_RELEASE, 0, acquisition->claimEpoch, + acquisition->wireID, LG_NET_FILE_LEASE_WIRE_SIZE, false) || + !lgNetFileLeaseEncode(send.payload.data(), send.payload.size(), &lease)) + return ClipboardChannelResult::FAILED; + std::vector batch; + try + { + batch.emplace_back(std::move(send)); + } + catch (const std::bad_alloc&) + { + return ClipboardChannelResult::FAILED; + } + if (!QueueSendBatchNL(batch)) + { + m_sourceBlocked = true; + m_sourceBlockedService = LG_NET_SERVICE_FILE; + m_sourceBlockedTransfer = record.transfer; + return ClipboardChannelResult::BUSY; + } + const uint64_t offerID = acquisition->offerID; + m_fileAcquisitions.erase( + std::remove_if(m_fileAcquisitions.begin(), m_fileAcquisitions.end(), + [&record](const FileAcquisition& value) + { return value.localID == record.transfer; }), + m_fileAcquisitions.end()); + m_fileTransfers.erase( + std::remove_if(m_fileTransfers.begin(), m_fileTransfers.end(), + [offerID](const FileTransfer& value) + { + return value.direction == Direction::HELPER_REQUESTER && + value.offerID == offerID; + }), + m_fileTransfers.end()); + return ClipboardChannelResult::ACCEPTED; + } + + case KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST: + { + if (!kvmfrClipboardTransferFromHelper(record.transfer) || + FindFileLocalNL(record.transfer) || + m_fileTransfers.size() >= MAX_REQUESTS) + return ClipboardChannelResult::FAILED; + bool acquired = false; + for (const FileAcquisition& acquisition : m_fileAcquisitions) + if (acquisition.direction == Direction::HELPER_REQUESTER && + acquisition.offerID == record.clipboardGeneration && acquisition.active) + { + acquired = true; + break; + } + if (!acquired) + return ClipboardChannelResult::FAILED; + + uint64_t entryID; + if (!lgNetFileEntryIDFromKVMFRNode(record.size, &entryID)) + return ClipboardChannelResult::FAILED; + const KVMFRClipboardFileOperation operation = + static_cast(record.token); + + FileTransfer transfer; + transfer.peer = m_owner; + transfer.direction = Direction::HELPER_REQUESTER; + transfer.claimEpoch = m_claimEpoch; + transfer.wireID = NextWireNL(); + transfer.localID = record.transfer; + transfer.offerID = record.clipboardGeneration; + transfer.entryID = entryID; + transfer.requestOffset = record.offset; + transfer.length = + operation == KVMFR_CLIPBOARD_FILE_OP_READ ? record.flags : 0; + transfer.operation = operation; + + LGNetFileRequest request = {}; + request.requestID = transfer.wireID; + request.offerID = transfer.offerID; + request.entryID = transfer.entryID; + request.offset = transfer.requestOffset; + request.length = transfer.length; + request.flags = + lgNetFileRequestFlagsFromKVMFROperation(transfer.operation); + SendItem send; + if (!BuildSendNL(send, transfer.peer, LG_NET_SERVICE_FILE, + LG_NET_FILE_MESSAGE_REQUEST, LG_NET_ENVELOPE_ACK_REQUIRED, + transfer.claimEpoch, transfer.wireID, LG_NET_FILE_REQUEST_WIRE_SIZE, + false) || + !lgNetFileRequestEncode( + send.payload.data(), send.payload.size(), &request)) + return ClipboardChannelResult::FAILED; + std::vector batch; + try + { + batch.emplace_back(std::move(send)); + } + catch (const std::bad_alloc&) + { + return ClipboardChannelResult::FAILED; + } + if (!QueueSendBatchNL(batch)) + { + m_sourceBlocked = true; + m_sourceBlockedService = LG_NET_SERVICE_FILE; + m_sourceBlockedTransfer = record.transfer; + return ClipboardChannelResult::BUSY; + } + try + { + m_fileTransfers.emplace_back(transfer); + } + catch (const std::bad_alloc&) + { + m_failed = true; + return ClipboardChannelResult::FAILED; + } + return ClipboardChannelResult::ACCEPTED; + } + + case KVMFR_CLIPBOARD_MESSAGE_FILE_DATA: + { + FileTransfer * transfer = FindFileLocalNL(record.transfer); + if (!transfer || transfer->direction != Direction::REMOTE_REQUESTER || + transfer->offerID != record.clipboardGeneration || + transfer->operation != record.token || + transfer->responseOffset != record.offset || + transfer->sequence != record.sequence || + (!transfer->began && !(record.flags & KVMFR_CLIPBOARD_FLAG_BEGIN)) || + (transfer->began && (record.flags & KVMFR_CLIPBOARD_FLAG_BEGIN))) + return ClipboardChannelResult::FAILED; + + const uint64_t sizeHint = + transfer->began ? transfer->sizeHint : record.size; + const uint64_t end = record.offset + record.length; + if (end > sizeHint || + (transfer->operation == KVMFR_CLIPBOARD_FILE_OP_READ && + (sizeHint > MAX_FILE_READ_LENGTH || end > transfer->length)) || + ((record.flags & KVMFR_CLIPBOARD_FLAG_END) && record.size != end)) + return ClipboardChannelResult::FAILED; + + std::vector batch; + if (!transfer->began) + { + LGNetFileTransfer begin = {}; + begin.requestID = transfer->wireID; + begin.offerID = transfer->offerID; + begin.entryID = transfer->entryID; + begin.offset = transfer->requestOffset; + begin.totalLength = transfer->requestOffset + record.size; + begin.processedLength = 0; + SendItem send; + if (!BuildSendForDeliveryNL(send, transfer->peer, LG_NET_SERVICE_FILE, + LG_NET_FILE_MESSAGE_DATA_BEGIN, 0, transfer->claimEpoch, + transfer->wireID, LG_NET_FILE_TRANSFER_WIRE_SIZE, + transfer->bulk) || + !lgNetFileTransferEncode( + send.payload.data(), send.payload.size(), &begin)) + return ClipboardChannelResult::FAILED; + try + { + batch.emplace_back(std::move(send)); + } + catch (const std::bad_alloc&) + { + return ClipboardChannelResult::FAILED; + } + } + + uint32_t consumed = 0; + const uint64_t wireLength = + (record.flags & KVMFR_CLIPBOARD_FLAG_END) ? end : sizeHint; + while (consumed < record.length) + { + const uint32_t length = + (std::min)(transfer->maxChunk, record.length - consumed); + LGNetFileChunk chunk = {}; + chunk.requestID = transfer->wireID; + chunk.offerID = transfer->offerID; + chunk.entryID = transfer->entryID; + chunk.offset = record.offset + consumed; + chunk.totalLength = wireLength; + chunk.dataLength = length; + chunk.flags = chunk.offset ? 0 : LG_NET_FILE_CHUNK_FIRST; + if ((record.flags & KVMFR_CLIPBOARD_FLAG_END) && + consumed + length == record.length) + chunk.flags |= LG_NET_FILE_CHUNK_FINAL; + chunk.data = data + consumed; + const size_t payloadLength = lgNetFileChunkSize(&chunk); + if (!payloadLength || payloadLength > UINT32_MAX) + return ClipboardChannelResult::FAILED; + SendItem send; + if (!BuildSendForDeliveryNL(send, transfer->peer, LG_NET_SERVICE_FILE, + LG_NET_FILE_MESSAGE_DATA_CHUNK, 0, transfer->claimEpoch, + transfer->wireID, static_cast(payloadLength), + transfer->bulk) || + !lgNetFileChunkEncode(send.payload.data(), send.payload.size(), &chunk)) + return ClipboardChannelResult::FAILED; + try + { + batch.emplace_back(std::move(send)); + } + catch (const std::bad_alloc&) + { + return ClipboardChannelResult::FAILED; + } + consumed += length; + } + + if (record.flags & KVMFR_CLIPBOARD_FLAG_END) + { + LGNetFileTransfer finish = {}; + finish.requestID = transfer->wireID; + finish.offerID = transfer->offerID; + finish.entryID = transfer->entryID; + finish.offset = transfer->requestOffset; + finish.totalLength = transfer->requestOffset + end; + finish.processedLength = end; + SendItem send; + if (!BuildSendForDeliveryNL(send, transfer->peer, LG_NET_SERVICE_FILE, + LG_NET_FILE_MESSAGE_DATA_END, 0, transfer->claimEpoch, + transfer->wireID, LG_NET_FILE_TRANSFER_WIRE_SIZE, + transfer->bulk) || + !lgNetFileTransferEncode( + send.payload.data(), send.payload.size(), &finish)) + return ClipboardChannelResult::FAILED; + try + { + batch.emplace_back(std::move(send)); + } + catch (const std::bad_alloc&) + { + return ClipboardChannelResult::FAILED; + } + } + + if (!QueueSendBatchNL(batch)) + { + m_sourceBlocked = true; + m_sourceBlockedService = LG_NET_SERVICE_FILE; + m_sourceBlockedTransfer = record.transfer; + return ClipboardChannelResult::BUSY; + } + + transfer = FindFileLocalNL(record.transfer); + if (!transfer) + return ClipboardChannelResult::ACCEPTED; + if (!transfer->began) + { + transfer->began = true; + transfer->sizeHint = record.size; + } + transfer->responseOffset += record.length; + ++transfer->sequence; + if (record.flags & KVMFR_CLIPBOARD_FLAG_END) + m_fileTransfers.erase( + std::remove_if(m_fileTransfers.begin(), m_fileTransfers.end(), + [&record](const FileTransfer& value) + { return value.localID == record.transfer; }), + m_fileTransfers.end()); + return ClipboardChannelResult::ACCEPTED; + } + + case KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL: + { + FileTransfer * transfer = FindFileLocalNL(record.transfer); + FileAcquisition * acquisition = FindAcquisitionLocalNL(record.transfer); + std::vector batch; + if (transfer) + { + LGNetFileTransfer wire = {}; + wire.requestID = transfer->wireID; + wire.offerID = transfer->offerID; + wire.entryID = transfer->entryID; + wire.offset = transfer->requestOffset; + wire.totalLength = transfer->requestOffset + + (transfer->sizeHint == KVMFR_CLIPBOARD_SIZE_UNKNOWN ? + transfer->responseOffset : + transfer->sizeHint); + wire.processedLength = transfer->responseOffset; + SendItem send; + if (!BuildSendNL(send, transfer->peer, LG_NET_SERVICE_FILE, + LG_NET_FILE_MESSAGE_CANCEL, 0, transfer->claimEpoch, + transfer->wireID, LG_NET_FILE_TRANSFER_WIRE_SIZE, false) || + !lgNetFileTransferEncode( + send.payload.data(), send.payload.size(), &wire)) + return ClipboardChannelResult::FAILED; + try + { + batch.emplace_back(std::move(send)); + } + catch (const std::bad_alloc&) + { + return ClipboardChannelResult::FAILED; + } + } + else if (acquisition) + { + if (!QueueFileStatusNL(acquisition->peer, acquisition->claimEpoch, + acquisition->wireID, acquisition->offerID, 0, + static_cast(record.token), 0, batch)) + return ClipboardChannelResult::FAILED; + } + else + return ClipboardChannelResult::ACCEPTED; + + if (!QueueSendBatchNL(batch)) + { + m_sourceBlocked = true; + m_sourceBlockedService = LG_NET_SERVICE_FILE; + m_sourceBlockedTransfer = record.transfer; + return ClipboardChannelResult::BUSY; + } + m_fileTransfers.erase( + std::remove_if(m_fileTransfers.begin(), m_fileTransfers.end(), + [&record](const FileTransfer& value) + { return value.localID == record.transfer; }), + m_fileTransfers.end()); + m_fileAcquisitions.erase( + std::remove_if(m_fileAcquisitions.begin(), m_fileAcquisitions.end(), + [&record](const FileAcquisition& value) + { return value.localID == record.transfer; }), + m_fileAcquisitions.end()); + return ClipboardChannelResult::ACCEPTED; + } + + default: + return ClipboardChannelResult::FAILED; + } +} + +CNetworkClipboard::CNetworkClipboard() : m_impl(std::make_unique()) +{ + const uint64_t seed = + GetTickCount64() ^ static_cast(reinterpret_cast(this)); + m_impl->m_nextSerial = seed ? seed : 1; + m_impl->m_nextLocal = seed ^ UINT64_C(0x4b564d4652434c50); + m_impl->m_nextWire = seed ^ UINT64_C(0x4e4554434c495042); + m_impl->m_nextEpoch = seed ^ UINT64_C(0x46494c4545504f43); +} + +CNetworkClipboard::~CNetworkClipboard() +{ + Stop(); + DetachSender(); +} + +bool CNetworkClipboard::AttachSender(INetworkClipboardSender& sender) +{ + CSRWExclusiveLock lifecycleLock(m_impl->m_lifecycleLock); + CSRWExclusiveLock processLock(m_impl->m_processLock); + CSRWExclusiveLock callbackLock(m_impl->m_senderCallbackLock); + CSRWExclusiveLock lock(m_impl->m_lock); + if (m_impl->m_sender) + return false; + m_impl->m_sender = &sender; + return true; +} + +void CNetworkClipboard::DetachSender() +{ + CSRWExclusiveLock lifecycleLock(m_impl->m_lifecycleLock); + CSRWExclusiveLock processLock(m_impl->m_processLock); + CSRWExclusiveLock callbackLock(m_impl->m_senderCallbackLock); + CSRWExclusiveLock lock(m_impl->m_lock); + if (!m_impl->m_sender) + return; + m_impl->DropOwnerNL(true); + m_impl->m_sender = nullptr; + m_impl->m_peers.clear(); + m_impl->m_sends.clear(); + m_impl->m_sendBytes = 0; +} + +bool CNetworkClipboard::PeerConnected(const NetworkClipboardPeer& peer) +{ + if (!peer.client || !peer.generation) + return false; + + NetworkClipboardPeerInfo info; + { + CSRWExclusiveLock callbackLock(m_impl->m_senderCallbackLock); + INetworkClipboardSender * sender; + { + CSRWSharedLock lock(m_impl->m_lock); + sender = m_impl->m_sender; + } + if (!sender || !sender->ClipboardPeerInfo(peer, info)) + return false; + } + + if (info.clipboardVersion < + LG_NET_CLIPBOARD_CHUNK_INTRODUCED_SERVICE_VERSION || + !(info.clipboardFlags & LG_NET_CAPABILITY_RELIABLE) || + info.clipboardReliablePayload < LG_NET_CLIPBOARD_OFFER_HEADER_WIRE_SIZE + + LG_NET_CLIPBOARD_MAX_MIME_LENGTH) + return false; + if (info.fileVersion < LG_NET_FILE_CHUNK_INTRODUCED_SERVICE_VERSION || + !(info.fileFlags & LG_NET_CAPABILITY_RELIABLE) || + info.fileReliablePayload < LG_NET_FILE_TRANSFER_WIRE_SIZE) + return false; + + CSRWExclusiveLock processLock(m_impl->m_processLock); + CSRWExclusiveLock lock(m_impl->m_lock); + if (!m_impl->m_sender || m_impl->FindPeerNL(peer)) + return false; + Impl::PeerState state; + state.peer = peer; + state.info = info; + if (m_impl->ChunkLengthNL(state, LG_NET_SERVICE_CLIPBOARD, + LG_NET_CLIPBOARD_CHUNK_HEADER_WIRE_SIZE, false) < MIN_CHUNK_LENGTH || + m_impl->ChunkLengthNL(state, LG_NET_SERVICE_FILE, + LG_NET_FILE_CHUNK_HEADER_WIRE_SIZE, false) < MIN_CHUNK_LENGTH) + return false; + try + { + m_impl->m_peers.emplace_back(state); + } + catch (const std::bad_alloc&) + { + return false; + } + return true; +} + +void CNetworkClipboard::PeerDisconnected(const NetworkClipboardPeer& peer) +{ + CSRWExclusiveLock processLock(m_impl->m_processLock); + { + CSRWExclusiveLock lock(m_impl->m_lock); + if (m_impl->m_claimEpoch && SamePeer(m_impl->m_owner, peer)) + m_impl->DropOwnerNL(true); + m_impl->m_peers.erase( + std::remove_if(m_impl->m_peers.begin(), m_impl->m_peers.end(), + [&peer](const Impl::PeerState& state) + { return SamePeer(state.peer, peer); }), + m_impl->m_peers.end()); + m_impl->m_sends.erase( + std::remove_if(m_impl->m_sends.begin(), m_impl->m_sends.end(), + [this, &peer](const Impl::SendItem& item) + { + if (!SamePeer(item.peer, peer)) + return false; + m_impl->m_sendBytes -= item.payload.size(); + return true; + }), + m_impl->m_sends.end()); + } + m_impl->PumpTargets(); + processLock.Unlock(); + m_impl->NotifyTargetReady(); + m_impl->NotifyFailure(); +} + +NetworkClipboardPacketResult CNetworkClipboard::HandlePacket( + const NetworkClipboardPeer& peer, const LGNetPacketView& packet, bool bulk) +{ + if ((packet.envelope.service != LG_NET_SERVICE_CLIPBOARD && + packet.envelope.service != LG_NET_SERVICE_FILE) || + (packet.payloadLength && !packet.payload) || + packet.payloadLength != packet.envelope.payloadLength || + packet.payloadLength > LG_NET_MAX_PAYLOAD_LENGTH) + return NetworkClipboardPacketResult::INVALID; + + CSRWExclusiveLock processLock(m_impl->m_processLock); + NetworkClipboardPacketResult result; + { + CSRWExclusiveLock lock(m_impl->m_lock); + Impl::PeerState * state = m_impl->FindPeerNL(peer); + if (!state) + return NetworkClipboardPacketResult::IGNORED; + + const uint16_t version = + packet.envelope.service == LG_NET_SERVICE_CLIPBOARD ? + state->info.clipboardVersion : + state->info.fileVersion; + const LGNetCapabilityFlags flags = + packet.envelope.service == LG_NET_SERVICE_CLIPBOARD ? + state->info.clipboardFlags : + state->info.fileFlags; + const uint32_t maximum = + packet.envelope.service == LG_NET_SERVICE_CLIPBOARD ? + (bulk ? state->info.clipboardBulkPayload : + state->info.clipboardReliablePayload) : + (bulk ? state->info.fileBulkPayload : state->info.fileReliablePayload); + if (!version || packet.envelope.serviceVersion != version || + !(flags & (bulk ? LG_NET_CAPABILITY_BULK : LG_NET_CAPABILITY_RELIABLE)) || + packet.payloadLength > maximum) + return NetworkClipboardPacketResult::INVALID; + + result = packet.envelope.service == LG_NET_SERVICE_CLIPBOARD ? + m_impl->HandleClipboardNL(*state, packet, bulk, GetTickCount64()) : + m_impl->HandleFileNL(*state, packet, bulk); + } + + m_impl->PumpTargets(); + m_impl->PumpSends(); + processLock.Unlock(); + m_impl->NotifyTargetReady(); + m_impl->NotifyFailure(); + return result; +} + +void CNetworkClipboard::Process(uint64_t nowMs) +{ + CSRWExclusiveLock processLock = CSRWExclusiveLock::Try(m_impl->m_processLock); + if (!processLock) + return; + + { + CSRWExclusiveLock lock(m_impl->m_lock); + if (m_impl->m_claimEpoch && DeadlineReached(nowMs, m_impl->m_claimDeadline)) + { + const NetworkClipboardPeer owner = m_impl->m_owner; + const uint64_t epoch = m_impl->m_claimEpoch; + const uint64_t serial = m_impl->m_claimSerial; + m_impl->DropOwnerNL(true); + m_impl->m_claimSerial = serial; + if (!m_impl->QueueClaimStatusNL( + owner, epoch, LG_NET_CLIPBOARD_CLAIM_EXPIRED, 0, 0, 0)) + m_impl->m_failed = true; + m_impl->m_claimSerial = 0; + } + } + + m_impl->PumpTargets(); + m_impl->PumpSends(); + processLock.Unlock(); + m_impl->NotifyTargetReady(); + m_impl->NotifyFailure(); +} + +void CNetworkClipboard::SendReady( + const NetworkClipboardPeer& peer, LGNetService service, bool bulk) +{ + if (service != LG_NET_SERVICE_CLIPBOARD && service != LG_NET_SERVICE_FILE) + return; + CSRWExclusiveLock processLock = CSRWExclusiveLock::Try(m_impl->m_processLock); + if (!processLock) + return; + { + CSRWSharedLock lock(m_impl->m_lock); + if (!m_impl->FindPeerNL(peer)) + return; + } + (void)bulk; + m_impl->PumpSends(); + processLock.Unlock(); + m_impl->NotifyTargetReady(); + m_impl->NotifyFailure(); +} + +bool CNetworkClipboard::Start(IClipboardTarget& target) +{ + CSRWExclusiveLock lifecycleLock(m_impl->m_lifecycleLock); + CSRWExclusiveLock processLock(m_impl->m_processLock); + CSRWExclusiveLock callbackLock(m_impl->m_targetCallbackLock); + CSRWExclusiveLock lock(m_impl->m_lock); + if (m_impl->m_started || m_impl->m_target) + return false; + m_impl->m_target = ⌖ + m_impl->m_started = true; + m_impl->m_failed = false; + m_impl->m_failureNotified = false; + return true; +} + +void CNetworkClipboard::Stop() +{ + CSRWExclusiveLock lifecycleLock(m_impl->m_lifecycleLock); + CSRWExclusiveLock processLock(m_impl->m_processLock); + CSRWExclusiveLock callbackLock(m_impl->m_targetCallbackLock); + CSRWExclusiveLock lock(m_impl->m_lock); + m_impl->m_target = nullptr; + m_impl->m_started = false; + m_impl->m_stateKnown = false; + m_impl->m_available = false; + m_impl->m_generation = 0; + m_impl->m_failed = false; + m_impl->m_failureNotified = false; + m_impl->m_targetCallbackActive = false; + m_impl->m_targets.clear(); + m_impl->m_targetBytes = 0; + m_impl->ResetTransfersNL(); + m_impl->m_remoteOffer.published = false; + m_impl->m_sourceBlocked = false; + m_impl->m_sourceBlockedService = 0; + m_impl->m_sourceBlockedTransfer = 0; + m_impl->m_receiveReadyPending = false; +} + +void CNetworkClipboard::ClipboardState(bool available, uint32_t generation) +{ + if (!generation) + return; + CSRWExclusiveLock processLock(m_impl->m_processLock); + { + CSRWExclusiveLock lock(m_impl->m_lock); + if (!m_impl->m_started || !m_impl->m_target) + return; + const bool generationChanged = m_impl->m_stateKnown && + m_impl->m_generation != generation; + const bool changed = !m_impl->m_stateKnown || + m_impl->m_generation != generation || m_impl->m_available != available; + m_impl->m_stateKnown = true; + m_impl->m_available = available; + m_impl->m_generation = generation; + if (!changed) + return; + + m_impl->m_targets.clear(); + m_impl->m_targetBytes = 0; + m_impl->ResetTransfersNL(); + m_impl->m_sourceBlocked = false; + m_impl->m_sourceBlockedService = 0; + m_impl->m_sourceBlockedTransfer = 0; + m_impl->m_receiveReadyPending = false; + m_impl->m_remoteOffer.published = false; + if (generationChanged && m_impl->m_claimEpoch) + { + const NetworkClipboardPeer owner = m_impl->m_owner; + const uint64_t epoch = m_impl->m_claimEpoch; + const uint64_t serial = m_impl->m_claimSerial; + m_impl->DropOwnerNL(false); + m_impl->m_claimSerial = serial; + if (!m_impl->QueueClaimStatusNL(owner, epoch, + LG_NET_CLIPBOARD_CLAIM_EXPIRED, 0, 0, 0)) + m_impl->m_failed = true; + m_impl->m_claimSerial = 0; + } + if (available && !m_impl->PublishRemoteOfferNL()) + { + m_impl->m_failed = true; + } + } + m_impl->PumpTargets(); + processLock.Unlock(); + m_impl->NotifyTargetReady(); + m_impl->NotifyFailure(); +} + +ClipboardChannelResult CNetworkClipboard::SendClipboard( + const KVMFRClipboardMessage& record, const uint8_t * data) +{ + CSRWExclusiveLock processLock(m_impl->m_processLock); + ClipboardChannelResult result; + { + CSRWExclusiveLock lock(m_impl->m_lock); + if (!m_impl->m_started || !m_impl->m_target || !m_impl->m_stateKnown || + !m_impl->m_available || !m_impl->m_generation || m_impl->m_failed || + record.generation != m_impl->m_generation || + record.version != KVMFR_CLIPBOARD_VERSION || (record.length && !data)) + return ClipboardChannelResult::FAILED; + + result = kvmfrClipboardFileMessageType(record.type) ? + m_impl->HandleHelperFileNL(record, data) : + m_impl->HandleHelperClipboardNL(record, data); + } + m_impl->PumpSends(); + processLock.Unlock(); + m_impl->NotifyTargetReady(); + m_impl->NotifyFailure(); + return result; +} + +void CNetworkClipboard::ClipboardReset(uint32_t generation, uint32_t reason) +{ + (void)reason; + bool available; + { + CSRWSharedLock lock(m_impl->m_lock); + available = m_impl->m_available; + } + ClipboardState(available, generation); +} diff --git a/idd/LGIdd/transport/network/CNetworkClipboard.h b/idd/LGIdd/transport/network/CNetworkClipboard.h new file mode 100644 index 00000000..6a8439bf --- /dev/null +++ b/idd/LGIdd/transport/network/CNetworkClipboard.h @@ -0,0 +1,105 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#pragma once + +#include "transport/IClipboardSource.h" + +#include + +#include +#include + +struct NetworkClipboardPeer +{ + uint32_t client = 0; + uint32_t generation = 0; +}; + +struct NetworkClipboardPeerInfo +{ + uint16_t clipboardVersion = 0; + uint16_t fileVersion = 0; + LGNetCapabilityFlags clipboardFlags = 0; + LGNetCapabilityFlags fileFlags = 0; + uint32_t clipboardReliablePayload = 0; + uint32_t clipboardBulkPayload = 0; + uint32_t fileReliablePayload = 0; + uint32_t fileBulkPayload = 0; +}; + +enum class NetworkClipboardSendResult +{ + ACCEPTED, + BLOCKED, + FAILED, +}; + +enum class NetworkClipboardPacketResult +{ + HANDLED, + IGNORED, + INVALID, +}; + +class INetworkClipboardSender +{ +public: + virtual ~INetworkClipboardSender() = default; + + virtual bool ClipboardPeerInfo( + const NetworkClipboardPeer& peer, NetworkClipboardPeerInfo& info) = 0; + virtual NetworkClipboardSendResult SendClipboard( + const NetworkClipboardPeer& peer, bool bulk, LGNetService service, + uint16_t messageType, LGNetEnvelopeFlags flags, uint64_t componentEpoch, + uint64_t requestID, const void * payload, uint32_t payloadLength) = 0; +}; + +class CNetworkClipboard final : public IClipboardSource +{ +private: + struct Impl; + std::unique_ptr m_impl; + +public: + CNetworkClipboard(); + ~CNetworkClipboard() override; + + CNetworkClipboard(const CNetworkClipboard&) = delete; + CNetworkClipboard& operator=(const CNetworkClipboard&) = delete; + + bool AttachSender(INetworkClipboardSender& sender); + void DetachSender(); + + bool PeerConnected(const NetworkClipboardPeer& peer); + void PeerDisconnected(const NetworkClipboardPeer& peer); + NetworkClipboardPacketResult HandlePacket( + const NetworkClipboardPeer& peer, const LGNetPacketView& packet, bool bulk); + void Process(uint64_t nowMs); + void SendReady( + const NetworkClipboardPeer& peer, LGNetService service, bool bulk); + + bool Start(IClipboardTarget& target) override; + void Stop() override; + void ClipboardState(bool available, uint32_t generation) override; + ClipboardChannelResult SendClipboard( + const KVMFRClipboardMessage& record, const uint8_t * data) override; + void ClipboardReset(uint32_t generation, uint32_t reason) override; +}; diff --git a/idd/LGIdd/transport/network/CNetworkServer.cpp b/idd/LGIdd/transport/network/CNetworkServer.cpp index 9512b223..d746af4e 100644 --- a/idd/LGIdd/transport/network/CNetworkServer.cpp +++ b/idd/LGIdd/transport/network/CNetworkServer.cpp @@ -49,32 +49,33 @@ namespace { - static constexpr uint64_t NETWORK_HANDSHAKE_TIMEOUT_MS = 5000; - static constexpr uint64_t NETWORK_IDLE_TIMEOUT_MS = 30000; - static constexpr uint32_t NETWORK_KEEPALIVE_INTERVAL_MS = 1000; - static constexpr uint32_t NETWORK_INITIAL_RTT_MS = 1; - static constexpr uint16_t NETWORK_PEER_BIDI_STREAMS = 16; - static constexpr uint16_t NETWORK_PEER_UNIDI_STREAMS = 16; - static constexpr uint32_t NETWORK_MAX_CONTROL_PAYLOAD = + static constexpr uint64_t NETWORK_HANDSHAKE_TIMEOUT_MS = 5000; + static constexpr uint64_t NETWORK_IDLE_TIMEOUT_MS = 30000; + static constexpr uint32_t NETWORK_KEEPALIVE_INTERVAL_MS = 1000; + static constexpr uint32_t NETWORK_INITIAL_RTT_MS = 1; + static constexpr uint16_t NETWORK_PEER_BIDI_STREAMS = 16; + static constexpr uint16_t NETWORK_PEER_UNIDI_STREAMS = 16; + static constexpr uint16_t NETWORK_BULK_STREAMS_PER_SERVICE = 1; + static constexpr uint32_t NETWORK_MAX_CONTROL_PAYLOAD = LG_NET_CURSOR_SHAPE_HEADER_WIRE_SIZE + LG_NET_CURSOR_MAX_SHAPE_LENGTH; - static constexpr size_t NETWORK_MAX_CONTROL_BUFFER = + static constexpr size_t NETWORK_MAX_CONTROL_BUFFER = LG_NET_ENVELOPE_WIRE_SIZE + NETWORK_MAX_CONTROL_PAYLOAD; - static constexpr uint32_t NETWORK_MAX_DATAGRAM_PAYLOAD = 1136; - static constexpr uint32_t NETWORK_INPUT_LEASE_MS = 500; - static constexpr uint8_t NETWORK_KEYBOARD_ROLLOVER = 0x01; - static constexpr uint16_t NETWORK_KEYBOARD_USAGE_PAGE = 0x07; - static constexpr size_t NETWORK_KEYBOARD_KEY_COUNT = 6; - static constexpr size_t NETWORK_SERVICE_COUNT = + static constexpr uint32_t NETWORK_MAX_DATAGRAM_PAYLOAD = 1136; + static constexpr uint32_t NETWORK_INPUT_LEASE_MS = 500; + static constexpr uint8_t NETWORK_KEYBOARD_ROLLOVER = 0x01; + static constexpr uint16_t NETWORK_KEYBOARD_USAGE_PAGE = 0x07; + static constexpr size_t NETWORK_KEYBOARD_KEY_COUNT = 6; + static constexpr size_t NETWORK_SERVICE_COUNT = LG_NET_SERVICE_CONTROL + 1U; - static constexpr size_t NETWORK_RECOVERY_REQUESTS = 16; - static constexpr uint64_t NETWORK_SEQUENCE_COMPONENT = UINT64_C(1); - static constexpr uint8_t NETWORK_DATAGRAM_WINDOW = 64; - static constexpr uint32_t NETWORK_PASSWORD_ROUNDS = 100000; - static constexpr uint16_t NETWORK_PASSWORD_SALT_SIZE = 32; - static constexpr uint16_t NETWORK_PASSWORD_CHALLENGE_SIZE = 32; - static constexpr uint16_t NETWORK_PASSWORD_NONCE_SIZE = 32; - static constexpr uint32_t NETWORK_AUTH_RETRY_AFTER_MS = 1000; + static constexpr size_t NETWORK_RECOVERY_REQUESTS = 16; + static constexpr uint64_t NETWORK_SEQUENCE_COMPONENT = UINT64_C(1); + static constexpr uint8_t NETWORK_DATAGRAM_WINDOW = 64; + static constexpr uint32_t NETWORK_PASSWORD_ROUNDS = 100000; + static constexpr uint16_t NETWORK_PASSWORD_SALT_SIZE = 32; + static constexpr uint16_t NETWORK_PASSWORD_CHALLENGE_SIZE = 32; + static constexpr uint16_t NETWORK_PASSWORD_NONCE_SIZE = 32; + static constexpr uint32_t NETWORK_AUTH_RETRY_AFTER_MS = 1000; static constexpr uint32_t NETWORK_SEND_RELIABLE_COUNT_MAX = 512; static constexpr uint64_t NETWORK_SEND_RELIABLE_BYTES_MAX = @@ -88,10 +89,13 @@ namespace static constexpr uint32_t NETWORK_SEND_DATAGRAM_COUNT_RESERVED = 64; static constexpr uint64_t NETWORK_SEND_DATAGRAM_BYTES_RESERVED = UINT64_C(256) * 1024U; + static constexpr uint32_t NETWORK_SEND_BULK_COUNT_MAX = 256; + static constexpr uint64_t NETWORK_SEND_BULK_BYTES_MAX = + UINT64_C(256) * 1024U * 1024U; static constexpr uint32_t NETWORK_SEND_RESUME_NUMERATOR = 3; static constexpr uint32_t NETWORK_SEND_RESUME_DENOMINATOR = 4; - static constexpr uint32_t NETWORK_KEYBOARD_LED_MASK = + static constexpr uint32_t NETWORK_KEYBOARD_LED_MASK = LG_NET_KEYBOARD_LED_NUM_LOCK | LG_NET_KEYBOARD_LED_CAPS_LOCK | LG_NET_KEYBOARD_LED_SCROLL_LOCK | @@ -232,14 +236,23 @@ namespace struct CNetworkServer::Impl { struct Connection; + struct BulkStream; + + enum class Delivery + { + RELIABLE, + DATAGRAM, + BULK, + }; struct Send { - QUIC_BUFFER buffer = {}; + QUIC_BUFFER buffer = {}; std::vector data; - LGNetService service = LG_NET_SERVICE_CORE; - uint32_t length = 0; - bool datagram = false; + LGNetService service = LG_NET_SERVICE_CORE; + uint32_t length = 0; + Delivery delivery = Delivery::RELIABLE; + bool accounted = true; }; struct SendLimits @@ -259,6 +272,29 @@ struct CNetworkServer::Impl uint16_t writableServices; }; + struct BulkStream + { + explicit BulkStream(Connection& connection) : connection(connection) {} + + Connection& connection; + BulkStream * next = nullptr; + HQUIC handle = nullptr; + LGNetService service = LG_NET_SERVICE_CORE; + uint64_t streamID = 0; + uint64_t sendSequence = 0; + uint64_t receiveSequence = 0; + bool outbound = false; + bool bound = false; + bool openingReturned = false; + bool shutdownComplete = false; + uint8_t bindingData[LG_NET_STREAM_BINDING_WIRE_SIZE] = {}; + size_t bindingLength = 0; + LGNetStreamParser receiveParser = {}; + + std::vector receiveData; + std::vector receivePayload; + }; + enum class SendResult { SENT, @@ -323,11 +359,19 @@ struct CNetworkServer::Impl uint16_t selectedServices = 0; uint16_t reliableServices = 0; uint16_t datagramServices = 0; - LGNetCapability offeredCapabilities [NETWORK_SERVICE_COUNT] = {}; - LGNetCapability selectedCapabilities[NETWORK_SERVICE_COUNT] = {}; + uint16_t bulkServices = 0; + uint16_t maxPeerBulkStreams = 0; + uint16_t peerBulkStreams = 0; + uint16_t servicePeerBulkStreams[NETWORK_SERVICE_COUNT] = {}; + LGNetCapability offeredCapabilities [NETWORK_SERVICE_COUNT] = {}; + LGNetCapability selectedCapabilities [NETWORK_SERVICE_COUNT] = {}; SendAccounting reliableSend = {}; SendAccounting datagramSend = {}; + SendAccounting bulkSend = {}; + + BulkStream * outboundBulk[NETWORK_SERVICE_COUNT] = {}; + BulkStream * inboundBulk = nullptr; bool datagramSendEnabled = false; bool closing = false; @@ -395,9 +439,10 @@ struct CNetworkServer::Impl }; Impl(CNetworkTransport& transport, CNetworkAudio& audio, - const NetworkSettings& settings) : + CNetworkClipboard& clipboard, const NetworkSettings& settings) : transport(transport), audio(audio), + clipboard(clipboard), settings(settings) { } @@ -417,6 +462,7 @@ struct CNetworkServer::Impl CNetworkTransport& transport; CNetworkAudio& audio; + CNetworkClipboard& clipboard; NetworkSettings settings; const QUIC_API_TABLE * api = nullptr; HQUIC registration = nullptr; @@ -481,6 +527,15 @@ struct CNetworkServer::Impl }; } + static NetworkClipboardPeer ClipboardPeer(const Connection& connection) + { + return + { + connection.client, + connection.generation, + }; + } + bool AudioPeerInfo(const NetworkAudioPeer& peer, NetworkAudioPeerInfo& info) { @@ -546,6 +601,84 @@ struct CNetworkServer::Impl return NetworkAudioSendResult::FAILED; } + bool ClipboardPeerInfo(const NetworkClipboardPeer& peer, + NetworkClipboardPeerInfo& info) + { + CSRWSharedLock guard(lock); + for (Connection * connection : connections) + { + if (connection->client != peer.client || + connection->generation != peer.generation) + continue; + + CSRWSharedLock connectionGuard(connection->lock); + const uint16_t clipboardMask = + UINT16_C(1) << LG_NET_SERVICE_CLIPBOARD; + const uint16_t fileMask = UINT16_C(1) << LG_NET_SERVICE_FILE; + if (connection->closing || + connection->handshake != HandshakeState::READY || + !(connection->selectedServices & clipboardMask) || + !(connection->selectedServices & fileMask)) + return false; + + const LGNetCapability& clipboardCapability = + connection->selectedCapabilities[LG_NET_SERVICE_CLIPBOARD]; + const LGNetCapability& fileCapability = + connection->selectedCapabilities[LG_NET_SERVICE_FILE]; + info.clipboardVersion = clipboardCapability.versionMin; + info.fileVersion = fileCapability.versionMin; + info.clipboardFlags = clipboardCapability.flags; + info.fileFlags = fileCapability.flags; + info.clipboardReliablePayload = clipboardCapability.maxPayloadLength; + info.fileReliablePayload = fileCapability.maxPayloadLength; + info.clipboardBulkPayload = + clipboardCapability.flags & LG_NET_CAPABILITY_BULK ? + clipboardCapability.maxPayloadLength : 0; + info.fileBulkPayload = + fileCapability.flags & LG_NET_CAPABILITY_BULK ? + fileCapability.maxPayloadLength : 0; + return true; + } + return false; + } + + NetworkClipboardSendResult SendClipboard( + const NetworkClipboardPeer& peer, bool bulk, LGNetService service, + uint16_t messageType, LGNetEnvelopeFlags flags, + uint64_t componentEpoch, uint64_t requestID, + const void * payload, uint32_t payloadLength) + { + if (service != LG_NET_SERVICE_CLIPBOARD && + service != LG_NET_SERVICE_FILE) + return NetworkClipboardSendResult::FAILED; + + CSRWSharedLock guard(lock); + for (Connection * connection : connections) + { + if (connection->client != peer.client || + connection->generation != peer.generation) + continue; + + const SendResult result = bulk ? + TrySendBulk(*connection, service, messageType, flags, + componentEpoch, requestID, payload, payloadLength) : + TrySendPacket(*connection, service, messageType, flags, + componentEpoch, requestID, payload, payloadLength); + switch (result) + { + case SendResult::SENT: + return NetworkClipboardSendResult::ACCEPTED; + + case SendResult::BLOCKED: + return NetworkClipboardSendResult::BLOCKED; + + case SendResult::FAILED: + return NetworkClipboardSendResult::FAILED; + } + } + return NetworkClipboardSendResult::FAILED; + } + void Shutdown(Connection& connection, QUIC_UINT62 error) { HQUIC handle = nullptr; @@ -561,9 +694,9 @@ struct CNetworkServer::Impl QUIC_CONNECTION_SHUTDOWN_FLAG_NONE, error); } - static SendLimits GetSendLimits(LGNetService service, bool datagram) + static SendLimits GetSendLimits(LGNetService service, Delivery delivery) { - if (datagram) + if (delivery == Delivery::DATAGRAM) { switch (service) { @@ -581,6 +714,23 @@ struct CNetworkServer::Impl } } + if (delivery == Delivery::BULK) + { + switch (service) + { + case LG_NET_SERVICE_VIDEO: + return { 128U, UINT64_C(192) * 1024U * 1024U }; + + case LG_NET_SERVICE_CLIPBOARD: + case LG_NET_SERVICE_FILE: + case LG_NET_SERVICE_USB: + return { 128U, UINT64_C(128) * 1024U * 1024U }; + + default: + return { 64U, UINT64_C(64) * 1024U * 1024U }; + } + } + switch (service) { case LG_NET_SERVICE_CURSOR: @@ -613,90 +763,167 @@ struct CNetworkServer::Impl service == LG_NET_SERVICE_CONTROL; } - static void BlockSendLocked(Connection& connection, - LGNetService service, bool datagram, uint64_t packetSize) + static bool BulkServiceSupported(LGNetService service) { - SendAccounting& accounting = datagram ? - connection.datagramSend : connection.reliableSend; - accounting.serviceBlocked[service] = true; - accounting.serviceBlockedBytes[service] = - accounting.serviceBlockedBytes[service] > packetSize ? - accounting.serviceBlockedBytes[service] : packetSize; - accounting.writableServices &= ~(UINT16_C(1) << service); + return service == LG_NET_SERVICE_VIDEO || + service == LG_NET_SERVICE_CLIPBOARD || + service == LG_NET_SERVICE_FILE; + } + + static bool BulkOriginAllowed(LGNetService service, LGNetRole origin) + { + if (service == LG_NET_SERVICE_VIDEO) + return origin == LG_NET_ROLE_SERVER; + return (service == LG_NET_SERVICE_CLIPBOARD || + service == LG_NET_SERVICE_FILE) && + (origin == LG_NET_ROLE_CLIENT || origin == LG_NET_ROLE_SERVER); + } + + static SendAccounting * GetSendAccountingLocked(Connection& connection, + Delivery delivery) + { + switch (delivery) + { + case Delivery::RELIABLE: + return &connection.reliableSend; + + case Delivery::DATAGRAM: + return &connection.datagramSend; + + case Delivery::BULK: + return &connection.bulkSend; + } + + return nullptr; + } + + static void BlockSendLocked(Connection& connection, + LGNetService service, Delivery delivery, uint64_t packetSize) + { + SendAccounting * accounting = + GetSendAccountingLocked(connection, delivery); + if (!accounting || service >= NETWORK_SERVICE_COUNT) + return; + + accounting->serviceBlocked[service] = true; + accounting->serviceBlockedBytes[service] = + accounting->serviceBlockedBytes[service] > packetSize ? + accounting->serviceBlockedBytes[service] : packetSize; + accounting->writableServices &= ~(UINT16_C(1) << service); } static SendResult ReserveSendLocked(Connection& connection, - LGNetService service, bool datagram, uint64_t packetSize) + LGNetService service, Delivery delivery, uint64_t packetSize) { if (service >= NETWORK_SERVICE_COUNT) return SendResult::FAILED; - const SendLimits limits = GetSendLimits(service, datagram); - uint32_t totalCountLimit = datagram ? - NETWORK_SEND_DATAGRAM_COUNT_MAX : NETWORK_SEND_RELIABLE_COUNT_MAX; - uint64_t totalByteLimit = datagram ? - NETWORK_SEND_DATAGRAM_BYTES_MAX : NETWORK_SEND_RELIABLE_BYTES_MAX; - if (!SendPriority(service)) + const SendLimits limits = GetSendLimits(service, delivery); + uint32_t totalCountLimit; + uint64_t totalByteLimit; + switch (delivery) { - totalCountLimit -= datagram ? + case Delivery::RELIABLE: + totalCountLimit = NETWORK_SEND_RELIABLE_COUNT_MAX; + totalByteLimit = NETWORK_SEND_RELIABLE_BYTES_MAX; + break; + + case Delivery::DATAGRAM: + totalCountLimit = NETWORK_SEND_DATAGRAM_COUNT_MAX; + totalByteLimit = NETWORK_SEND_DATAGRAM_BYTES_MAX; + break; + + case Delivery::BULK: + totalCountLimit = NETWORK_SEND_BULK_COUNT_MAX; + totalByteLimit = NETWORK_SEND_BULK_BYTES_MAX; + break; + } + if (delivery != Delivery::BULK && !SendPriority(service)) + { + totalCountLimit -= delivery == Delivery::DATAGRAM ? NETWORK_SEND_DATAGRAM_COUNT_RESERVED : NETWORK_SEND_RELIABLE_COUNT_RESERVED; - totalByteLimit -= datagram ? + totalByteLimit -= delivery == Delivery::DATAGRAM ? NETWORK_SEND_DATAGRAM_BYTES_RESERVED : NETWORK_SEND_RELIABLE_BYTES_RESERVED; } - SendAccounting& accounting = datagram ? - connection.datagramSend : connection.reliableSend; + SendAccounting * accounting = + GetSendAccountingLocked(connection, delivery); + if (!accounting) + return SendResult::FAILED; if (packetSize > limits.bytes || packetSize > totalByteLimit) return SendResult::FAILED; - if (accounting.count >= totalCountLimit || - accounting.serviceCount[service] >= limits.count || - accounting.bytes > totalByteLimit - packetSize || - accounting.serviceBytes[service] > limits.bytes - packetSize) + if (accounting->count >= totalCountLimit || + accounting->serviceCount[service] >= limits.count || + accounting->bytes > totalByteLimit - packetSize || + accounting->serviceBytes[service] > limits.bytes - packetSize) { - BlockSendLocked(connection, service, datagram, packetSize); + BlockSendLocked(connection, service, delivery, packetSize); return SendResult::BLOCKED; } - ++accounting.count; - ++accounting.serviceCount[service]; - accounting.bytes += packetSize; - accounting.serviceBytes[service] += packetSize; - accounting.writableServices &= ~(UINT16_C(1) << service); + ++accounting->count; + ++accounting->serviceCount[service]; + accounting->bytes += packetSize; + accounting->serviceBytes[service] += packetSize; + accounting->writableServices &= ~(UINT16_C(1) << service); return SendResult::SENT; } - static bool SendCanResumeLocked(const Connection& connection, - LGNetService service, bool datagram) + static bool SendCanResumeLocked(Connection& connection, + LGNetService service, Delivery delivery) { - const SendLimits limits = GetSendLimits(service, datagram); - uint32_t totalCountLimit = datagram ? - NETWORK_SEND_DATAGRAM_COUNT_MAX : NETWORK_SEND_RELIABLE_COUNT_MAX; - uint64_t totalByteLimit = datagram ? - NETWORK_SEND_DATAGRAM_BYTES_MAX : NETWORK_SEND_RELIABLE_BYTES_MAX; - if (!SendPriority(service)) + const SendLimits limits = GetSendLimits(service, delivery); + uint32_t totalCountLimit; + uint64_t totalByteLimit; + switch (delivery) { - totalCountLimit -= datagram ? + case Delivery::RELIABLE: + totalCountLimit = NETWORK_SEND_RELIABLE_COUNT_MAX; + totalByteLimit = NETWORK_SEND_RELIABLE_BYTES_MAX; + break; + + case Delivery::DATAGRAM: + totalCountLimit = NETWORK_SEND_DATAGRAM_COUNT_MAX; + totalByteLimit = NETWORK_SEND_DATAGRAM_BYTES_MAX; + break; + + case Delivery::BULK: + totalCountLimit = NETWORK_SEND_BULK_COUNT_MAX; + totalByteLimit = NETWORK_SEND_BULK_BYTES_MAX; + break; + } + if (delivery != Delivery::BULK && !SendPriority(service)) + { + totalCountLimit -= delivery == Delivery::DATAGRAM ? NETWORK_SEND_DATAGRAM_COUNT_RESERVED : NETWORK_SEND_RELIABLE_COUNT_RESERVED; - totalByteLimit -= datagram ? + totalByteLimit -= delivery == Delivery::DATAGRAM ? NETWORK_SEND_DATAGRAM_BYTES_RESERVED : NETWORK_SEND_RELIABLE_BYTES_RESERVED; } - const SendAccounting& accounting = datagram ? - connection.datagramSend : connection.reliableSend; + const SendAccounting * accounting = + GetSendAccountingLocked(connection, delivery); + if (!accounting) + return false; - const uint32_t totalCount = accounting.count; - const uint64_t totalBytes = accounting.bytes; - const uint32_t serviceCount = accounting.serviceCount[service]; - const uint64_t serviceBytes = accounting.serviceBytes[service]; - const uint64_t blockedBytes = accounting.serviceBlockedBytes[service]; + const uint32_t totalCount = accounting->count; + const uint64_t totalBytes = accounting->bytes; + const uint32_t serviceCount = accounting->serviceCount[service]; + const uint64_t serviceBytes = accounting->serviceBytes[service]; + const uint64_t blockedBytes = + accounting->serviceBlockedBytes[service]; - if (connection.closing || !connection.controlStream || - (datagram && (!connection.datagramSendEnabled || - blockedBytes > connection.maxDatagramSendLength))) + if (connection.closing || + (delivery == Delivery::RELIABLE && !connection.controlStream) || + (delivery == Delivery::DATAGRAM && + (!connection.datagramSendEnabled || + blockedBytes > connection.maxDatagramSendLength)) || + (delivery == Delivery::BULK && + (!connection.outboundBulk[service] || + !connection.outboundBulk[service]->bound))) return false; const uint32_t totalResumeCount = @@ -723,27 +950,33 @@ struct CNetworkServer::Impl } static uint16_t ReadyServicesLocked(Connection& connection, - bool datagram) + Delivery delivery) { - uint16_t ready = 0; - SendAccounting& accounting = datagram ? - connection.datagramSend : connection.reliableSend; + uint16_t ready = 0; + SendAccounting * accounting = + GetSendAccountingLocked(connection, delivery); + if (!accounting) + return 0; + for (LGNetService service = LG_NET_SERVICE_CORE; service < NETWORK_SERVICE_COUNT; ++service) { - if (!accounting.serviceBlocked[service] || !SendCanResumeLocked( - connection, service, datagram)) + if (!accounting->serviceBlocked[service] || !SendCanResumeLocked( + connection, service, delivery)) continue; - accounting.serviceBlocked[service] = false; - accounting.serviceBlockedBytes[service] = 0; - ready |= UINT16_C(1) << service; + accounting->serviceBlocked[service] = false; + accounting->serviceBlockedBytes[service] = 0; + ready |= UINT16_C(1) << service; } return ready; } static void ReleaseSendLocked(Connection& connection, const Send& send) { + if (!send.accounted) + return; + if (send.service >= NETWORK_SERVICE_COUNT) { DEBUG_ERROR("Network send accounting service out of range for " @@ -751,49 +984,70 @@ struct CNetworkServer::Impl return; } - SendAccounting& accounting = send.datagram ? - connection.datagramSend : connection.reliableSend; - if (!accounting.count || !accounting.serviceCount[send.service] || - accounting.bytes < send.length || - accounting.serviceBytes[send.service] < send.length) + SendAccounting * accounting = + GetSendAccountingLocked(connection, send.delivery); + if (!accounting) + { + DEBUG_ERROR("Network send accounting delivery out of range for " + "client %u", connection.client); + return; + } + if (!accounting->count || !accounting->serviceCount[send.service] || + accounting->bytes < send.length || + accounting->serviceBytes[send.service] < send.length) { DEBUG_ERROR("Network send accounting underflow for client %u", connection.client); return; } - --accounting.count; - --accounting.serviceCount[send.service]; - accounting.bytes -= send.length; - accounting.serviceBytes[send.service] -= send.length; - accounting.writableServices |= - ReadyServicesLocked(connection, send.datagram); + --accounting->count; + --accounting->serviceCount[send.service]; + accounting->bytes -= send.length; + accounting->serviceBytes[send.service] -= send.length; + accounting->writableServices |= + ReadyServicesLocked(connection, send.delivery); } static bool ConsumeSendReady(Connection& connection, - LGNetService service, bool datagram) + LGNetService service, Delivery delivery) { if (service >= NETWORK_SERVICE_COUNT) return false; CSRWExclusiveLock guard(connection.lock); - SendAccounting& accounting = datagram ? - connection.datagramSend : connection.reliableSend; + SendAccounting * accounting = + GetSendAccountingLocked(connection, delivery); + if (!accounting) + return false; const uint16_t serviceMask = UINT16_C(1) << service; const bool ready = - (accounting.writableServices & serviceMask) != 0; - accounting.writableServices &= ~serviceMask; + (accounting->writableServices & serviceMask) != 0; + accounting->writableServices &= ~serviceMask; return ready; } void NotifyAudioSendReady(Connection& connection, bool datagram) { - if (ConsumeSendReady( - connection, LG_NET_SERVICE_AUDIO, datagram)) + const Delivery delivery = datagram ? + Delivery::DATAGRAM : Delivery::RELIABLE; + if (ConsumeSendReady(connection, LG_NET_SERVICE_AUDIO, delivery)) audio.SendReady( AudioPeer(connection), LG_NET_SERVICE_AUDIO, datagram); } + void NotifyClipboardSendReady(Connection& connection, Delivery delivery) + { + const bool bulk = delivery == Delivery::BULK; + if (ConsumeSendReady( + connection, LG_NET_SERVICE_CLIPBOARD, delivery)) + clipboard.SendReady( + ClipboardPeer(connection), LG_NET_SERVICE_CLIPBOARD, bulk); + if (ConsumeSendReady(connection, LG_NET_SERVICE_FILE, delivery)) + clipboard.SendReady( + ClipboardPeer(connection), LG_NET_SERVICE_FILE, bulk); + } + SendResult TrySendPacket(Connection& connection, LGNetService service, uint16_t messageType, LGNetEnvelopeFlags flags, uint64_t componentEpoch, uint64_t requestID, const void * payload, @@ -840,7 +1094,8 @@ struct CNetworkServer::Impl return SendResult::FAILED; const SendResult reserve = - ReserveSendLocked(connection, service, false, packetSize); + ReserveSendLocked( + connection, service, Delivery::RELIABLE, packetSize); if (reserve != SendResult::SENT) return reserve; @@ -850,7 +1105,7 @@ struct CNetworkServer::Impl Send rollback; rollback.service = service; rollback.length = static_cast(packetSize); - rollback.datagram = false; + rollback.delivery = Delivery::RELIABLE; ReleaseSendLocked(connection, rollback); return SendResult::FAILED; } @@ -862,13 +1117,13 @@ struct CNetworkServer::Impl { send->service = service; send->length = static_cast(packetSize); - send->datagram = false; + send->delivery = Delivery::RELIABLE; ReleaseSendLocked(connection, *send); return SendResult::FAILED; } send->service = service; send->length = static_cast(packetSize); - send->datagram = false; + send->delivery = Delivery::RELIABLE; LGNetPacketParameters parameters; lgNetPacketParametersInit(¶meters, service, messageType, @@ -915,6 +1170,245 @@ struct CNetworkServer::Impl SendResult::SENT; } + static void DetachBulkStreamLocked(BulkStream& stream) + { + Connection& connection = stream.connection; + if (stream.outbound) + { + if (stream.service < NETWORK_SERVICE_COUNT && + connection.outboundBulk[stream.service] == &stream) + connection.outboundBulk[stream.service] = nullptr; + return; + } + + BulkStream ** link = &connection.inboundBulk; + while (*link && *link != &stream) + link = &(*link)->next; + if (*link != &stream) + return; + + *link = stream.next; + if (connection.peerBulkStreams) + --connection.peerBulkStreams; + if (stream.bound && stream.service < NETWORK_SERVICE_COUNT && + connection.servicePeerBulkStreams[stream.service]) + --connection.servicePeerBulkStreams[stream.service]; + } + + SendResult OpenBulkStream(Connection& connection, LGNetService service, + uint64_t packetSize) + { + std::unique_ptr stream( + new (std::nothrow) BulkStream(connection)); + if (!stream) + return SendResult::FAILED; + stream->service = service; + stream->outbound = true; + + HQUIC connectionHandle; + { + CSRWExclusiveLock guard(connection.lock); + const uint16_t serviceMask = UINT16_C(1) << service; + if (connection.closing || !connection.handle || + !BulkOriginAllowed(service, LG_NET_ROLE_SERVER) || + connection.handshake != HandshakeState::READY || + !(connection.selectedServices & serviceMask) || + !(connection.bulkServices & serviceMask) || + !connection.selectedCapabilities + [service].maxUnidirectionalStreams) + return SendResult::FAILED; + if (connection.outboundBulk[service]) + { + BlockSendLocked( + connection, service, Delivery::BULK, packetSize); + return SendResult::BLOCKED; + } + + connection.outboundBulk[service] = stream.get(); + connectionHandle = connection.handle; + BlockSendLocked(connection, service, Delivery::BULK, packetSize); + } + + HQUIC handle = nullptr; + QUIC_STATUS status = api->StreamOpen(connectionHandle, + QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL, BulkStreamCallback, + stream.get(), &handle); + if (QUIC_FAILED(status)) + { + CSRWExclusiveLock guard(connection.lock); + DetachBulkStreamLocked(*stream); + connection.bulkSend.serviceBlocked[service] = false; + connection.bulkSend.serviceBlockedBytes[service] = 0; + return SendResult::FAILED; + } + stream->handle = handle; + + status = api->StreamStart(handle, + QUIC_STREAM_START_FLAG_IMMEDIATE | + QUIC_STREAM_START_FLAG_SHUTDOWN_ON_FAIL); + if (QUIC_FAILED(status)) + { + { + CSRWExclusiveLock guard(connection.lock); + DetachBulkStreamLocked(*stream); + connection.bulkSend.serviceBlocked[service] = false; + connection.bulkSend.serviceBlockedBytes[service] = 0; + } + api->StreamClose(handle); + return SendResult::FAILED; + } + + bool bound; + bool shutdownComplete; + { + CSRWExclusiveLock guard(connection.lock); + stream->openingReturned = true; + bound = stream->bound && + !stream->shutdownComplete; + shutdownComplete = stream->shutdownComplete; + if (bound) + ReadyServicesLocked(connection, Delivery::BULK); + } + if (shutdownComplete) + return SendResult::FAILED; + + stream.release(); + return bound ? SendResult::SENT : SendResult::BLOCKED; + } + + SendResult TrySendBulk(Connection& connection, LGNetService service, + uint16_t messageType, LGNetEnvelopeFlags flags, + uint64_t componentEpoch, uint64_t requestID, const void * payload, + uint32_t payloadLength) + { + const LGNetServiceVersionInfo * version = + lgNetServiceVersionInfo(service); + const LGNetMessageRoute * route = + lgNetMessageRoute(service, messageType); + if (!version || + !lgNetMessageRouteAllows(route, LG_NET_ROLE_SERVER, + LG_NET_ROUTE_DELIVERY_BULK) || + (payloadLength && !payload) || + payloadLength > LG_NET_MAX_PAYLOAD_LENGTH || + flags & (LG_NET_ENVELOPE_RELIABLE | LG_NET_ENVELOPE_DATAGRAM | + LG_NET_ENVELOPE_EARLY_DATA)) + return SendResult::FAILED; + + const size_t packetSize = + LG_NET_ENVELOPE_WIRE_SIZE + payloadLength; + const SendLimits limits = + GetSendLimits(service, Delivery::BULK); + if (packetSize > limits.bytes || + packetSize > NETWORK_SEND_BULK_BYTES_MAX) + return SendResult::FAILED; + +retry: + std::unique_ptr send; + QUIC_STATUS status; + { + CSRWExclusiveLock guard(connection.lock); + const uint16_t serviceMask = UINT16_C(1) << service; + if (connection.closing || !connection.handle || + connection.handshake != HandshakeState::READY || + !(connection.selectedServices & serviceMask) || + !(connection.bulkServices & serviceMask) || + !connection.selectedCapabilities + [service].maxUnidirectionalStreams) + return SendResult::FAILED; + + const uint16_t serviceVersion = + connection.selectedCapabilities[service].versionMin; + if (payloadLength > connection.selectedCapabilities + [service].maxPayloadLength || + !lgNetServiceVersionSupported(service, serviceVersion) || + serviceVersion < route->serviceVersionMin) + return SendResult::FAILED; + + BulkStream * stream = connection.outboundBulk[service]; + if (!stream) + { + guard.Unlock(); + const SendResult open = + OpenBulkStream(connection, service, packetSize); + if (open == SendResult::SENT) + goto retry; + return open; + } + if (!stream->bound) + { + stream->openingReturned = true; + BlockSendLocked(connection, service, Delivery::BULK, packetSize); + return SendResult::BLOCKED; + } + if (stream->sendSequence == UINT64_MAX) + return SendResult::FAILED; + + const SendResult reserve = ReserveSendLocked( + connection, service, Delivery::BULK, packetSize); + if (reserve != SendResult::SENT) + return reserve; + + send.reset(new (std::nothrow) Send()); + if (!send) + { + Send rollback; + rollback.service = service; + rollback.length = static_cast(packetSize); + rollback.delivery = Delivery::BULK; + ReleaseSendLocked(connection, rollback); + return SendResult::FAILED; + } + try + { + send->data.resize(packetSize); + } + catch (...) + { + send->service = service; + send->length = static_cast(packetSize); + send->delivery = Delivery::BULK; + ReleaseSendLocked(connection, *send); + return SendResult::FAILED; + } + send->service = service; + send->length = static_cast(packetSize); + send->delivery = Delivery::BULK; + + LGNetPacketParameters parameters; + lgNetPacketParametersInit(¶meters, service, messageType, + serviceVersion, route->messageVersion); + parameters.flags = flags | LG_NET_ENVELOPE_FINAL; + parameters.sessionEpoch = connection.sessionEpoch; + parameters.componentEpoch = componentEpoch; + parameters.sequence = ++stream->sendSequence; + parameters.requestID = requestID; + + size_t wireSize; + if (!lgNetPacketConstruct(send->data.data(), send->data.size(), + &wireSize, ¶meters, payload, payloadLength) || + wireSize != packetSize) + { + --stream->sendSequence; + ReleaseSendLocked(connection, *send); + return SendResult::FAILED; + } + + send->buffer.Buffer = send->data.data(); + send->buffer.Length = static_cast(wireSize); + status = api->StreamSend(stream->handle, &send->buffer, 1, + QUIC_SEND_FLAG_NONE, send.get()); + if (QUIC_FAILED(status)) + { + --stream->sendSequence; + ReleaseSendLocked(connection, *send); + } + } + if (QUIC_FAILED(status)) + return SendResult::FAILED; + send.release(); + return SendResult::SENT; + } + SendResult TrySendDatagram(Connection& connection, LGNetService service, uint16_t messageType, LGNetEnvelopeFlags flags, uint64_t componentEpoch, uint64_t requestID, const void * payload, @@ -961,15 +1455,17 @@ struct CNetworkServer::Impl if (!connection.datagramSendEnabled || packetSize > connection.maxDatagramSendLength) { - const SendLimits limits = GetSendLimits(service, true); + const SendLimits limits = GetSendLimits(service, Delivery::DATAGRAM); if (packetSize <= limits.bytes && packetSize <= NETWORK_SEND_DATAGRAM_BYTES_MAX) - BlockSendLocked(connection, service, true, packetSize); + BlockSendLocked( + connection, service, Delivery::DATAGRAM, packetSize); return SendResult::BLOCKED; } const SendResult reserve = - ReserveSendLocked(connection, service, true, packetSize); + ReserveSendLocked( + connection, service, Delivery::DATAGRAM, packetSize); if (reserve != SendResult::SENT) return reserve; @@ -979,7 +1475,7 @@ struct CNetworkServer::Impl Send rollback; rollback.service = service; rollback.length = static_cast(packetSize); - rollback.datagram = true; + rollback.delivery = Delivery::DATAGRAM; ReleaseSendLocked(connection, rollback); return SendResult::FAILED; } @@ -991,13 +1487,13 @@ struct CNetworkServer::Impl { send->service = service; send->length = static_cast(packetSize); - send->datagram = true; + send->delivery = Delivery::DATAGRAM; ReleaseSendLocked(connection, *send); return SendResult::FAILED; } send->service = service; send->length = static_cast(packetSize); - send->datagram = true; + send->delivery = Delivery::DATAGRAM; LGNetPacketParameters parameters; lgNetPacketParametersInit(¶meters, service, messageType, @@ -1100,6 +1596,11 @@ struct CNetworkServer::Impl connection.selectedServices = 0; connection.reliableServices = 0; connection.datagramServices = 0; + connection.bulkServices = 0; + connection.maxPeerBulkStreams = 0; + connection.peerBulkStreams = 0; + memset(connection.servicePeerBulkStreams, 0, + sizeof(connection.servicePeerBulkStreams)); memcpy(connection.clientNonce, hello.nonce, sizeof(connection.clientNonce)); memcpy(connection.serverNonce, serverNonce, @@ -1120,7 +1621,13 @@ struct CNetworkServer::Impl capability.service <= LG_NET_SERVICE_CORE || capability.service > LG_NET_SERVICE_CONTROL || capability.service == LG_NET_SERVICE_USB || - capability.flags & LG_NET_CAPABILITY_SELECTED) + capability.flags & LG_NET_CAPABILITY_SELECTED || + capability.maxBidirectionalStreams || + capability.maxUnidirectionalStreams > NETWORK_PEER_UNIDI_STREAMS || + (!!(capability.flags & LG_NET_CAPABILITY_BULK) != + !!capability.maxUnidirectionalStreams) || + !(capability.flags & (LG_NET_CAPABILITY_RELIABLE | + LG_NET_CAPABILITY_DATAGRAM | LG_NET_CAPABILITY_BULK))) return false; const LGNetServiceVersionInfo * version = @@ -1155,6 +1662,10 @@ struct CNetworkServer::Impl uint32_t minimumPayload; switch (offered.service) { + case LG_NET_SERVICE_VIDEO: + minimumPayload = LG_NET_VIDEO_FRAGMENT_HEADER_WIRE_SIZE; + break; + case LG_NET_SERVICE_RECOVERY: minimumPayload = LG_NET_RECOVERY_REQUEST_WIRE_SIZE; break; @@ -1172,6 +1683,14 @@ struct CNetworkServer::Impl AUDIO_MAX_PCM_BYTES; break; + case LG_NET_SERVICE_CLIPBOARD: + minimumPayload = LG_NET_CLIPBOARD_OFFER_HEADER_WIRE_SIZE; + break; + + case LG_NET_SERVICE_FILE: + minimumPayload = LG_NET_FILE_ENTRY_HEADER_WIRE_SIZE; + break; + case LG_NET_SERVICE_CONTROL: minimumPayload = LG_NET_CONTROL_FRAME_SCHEDULE_WIRE_SIZE; break; @@ -1214,7 +1733,10 @@ struct CNetworkServer::Impl selected.maxBidirectionalStreams = 0; selected.maxUnidirectionalStreams = 0; const uint32_t minimumDatagram = offered.service == - LG_NET_SERVICE_INPUT ? LG_NET_INPUT_ABSOLUTE_WIRE_SIZE : + LG_NET_SERVICE_VIDEO ? + LG_NET_VIDEO_FRAGMENT_HEADER_WIRE_SIZE + 1U : + offered.service == LG_NET_SERVICE_INPUT ? + LG_NET_INPUT_ABSOLUTE_WIRE_SIZE : offered.service == LG_NET_SERVICE_CURSOR ? LG_NET_CURSOR_POSITION_WIRE_SIZE : offered.service == LG_NET_SERVICE_AUDIO ? @@ -1229,6 +1751,17 @@ struct CNetworkServer::Impl connection.maxDatagramPayload ? offered.maxDatagramPayload : connection.maxDatagramPayload; } + if (BulkServiceSupported(offered.service) && + (offered.flags & LG_NET_CAPABILITY_BULK) && + offered.maxUnidirectionalStreams) + { + selected.flags |= LG_NET_CAPABILITY_BULK; + selected.maxUnidirectionalStreams = + offered.maxUnidirectionalStreams < + NETWORK_BULK_STREAMS_PER_SERVICE ? + offered.maxUnidirectionalStreams : + NETWORK_BULK_STREAMS_PER_SERVICE; + } return lgNetCapabilityValid(&selected); } @@ -1358,8 +1891,9 @@ struct CNetworkServer::Impl if (!session) return false; - bool activated = false; - bool audioSelected = false; + bool activated = false; + bool audioSelected = false; + bool clipboardSelected = false; { CSRWExclusiveLock guard(connection.lock); activated = !connection.closing && @@ -1378,6 +1912,12 @@ struct CNetworkServer::Impl connection.handshake = HandshakeState::READY; audioSelected = (connection.selectedServices & (UINT16_C(1) << LG_NET_SERVICE_AUDIO)) != 0; + const uint16_t clipboardServices = + (UINT16_C(1) << LG_NET_SERVICE_CLIPBOARD) | + (UINT16_C(1) << LG_NET_SERVICE_FILE); + clipboardSelected = + (connection.selectedServices & clipboardServices) == + clipboardServices; } } if (!activated) @@ -1387,6 +1927,9 @@ struct CNetworkServer::Impl } if (audioSelected && !audio.PeerConnected(AudioPeer(connection))) return false; + if (clipboardSelected && + !clipboard.PeerConnected(ClipboardPeer(connection))) + return false; DEBUG_INFO("Network client %u completed the control handshake", connection.client); return true; @@ -1431,6 +1974,16 @@ struct CNetworkServer::Impl connection.reliableServices |= serviceMask; if (capability.flags & LG_NET_CAPABILITY_DATAGRAM) connection.datagramServices |= serviceMask; + if (capability.flags & LG_NET_CAPABILITY_BULK) + { + if (capability.maxUnidirectionalStreams > + NETWORK_PEER_UNIDI_STREAMS - + connection.maxPeerBulkStreams) + return false; + connection.bulkServices |= serviceMask; + connection.maxPeerBulkStreams += + capability.maxUnidirectionalStreams; + } ++connection.selectedCapabilityCount; } } @@ -2237,7 +2790,8 @@ struct CNetworkServer::Impl const LGNetInputStatusCode code = packet.envelope.messageType == LG_NET_INPUT_MESSAGE_RELEASE && result == InputResult::ACCEPTED ? - LG_NET_INPUT_STATUS_RELEASED : InputStatusCode(result); + static_cast(LG_NET_INPUT_STATUS_RELEASED) : + InputStatusCode(result); if (result != InputResult::ACCEPTED) InvalidateInputClaim(connection, control.claimEpoch); return SendInputStatus(connection, LG_NET_INPUT_MESSAGE_STATUS, @@ -2861,24 +3415,34 @@ struct CNetworkServer::Impl bool HandlePacket(Connection& connection, const LGNetPacketView& packet) { - const bool cursorStatus = + const bool cursorStatus = packet.envelope.service == LG_NET_SERVICE_CURSOR && packet.envelope.messageType == LG_NET_CURSOR_MESSAGE_STATUS; - const bool audioSubscribe = + const bool audioSubscribe = packet.envelope.service == LG_NET_SERVICE_AUDIO && packet.envelope.messageType == LG_NET_AUDIO_MESSAGE_SUBSCRIBE; - const bool audioResponse = + const bool audioResponse = packet.envelope.service == LG_NET_SERVICE_AUDIO && packet.envelope.messageType == LG_NET_AUDIO_MESSAGE_STATE_ACK; - LGNetEnvelopeFlags expectedFlags = + const bool clipboardPacket = + packet.envelope.service == LG_NET_SERVICE_CLIPBOARD || + packet.envelope.service == LG_NET_SERVICE_FILE; + LGNetEnvelopeFlags expectedFlags = LG_NET_ENVELOPE_RELIABLE | LG_NET_ENVELOPE_FINAL; if (audioSubscribe) expectedFlags |= LG_NET_ENVELOPE_ACK_REQUIRED; if (cursorStatus || audioResponse) expectedFlags |= LG_NET_ENVELOPE_RESPONSE; + constexpr LGNetEnvelopeFlags DELIVERY_FLAGS = + LG_NET_ENVELOPE_RELIABLE | LG_NET_ENVELOPE_DATAGRAM | + LG_NET_ENVELOPE_FINAL | LG_NET_ENVELOPE_ERROR | + LG_NET_ENVELOPE_EARLY_DATA; if (packet.envelope.headerSize != LG_NET_ENVELOPE_WIRE_SIZE || packet.envelope.payloadLength > NETWORK_MAX_CONTROL_PAYLOAD || - packet.envelope.flags != expectedFlags || + (clipboardPacket ? + (packet.envelope.flags & DELIVERY_FLAGS) != + (LG_NET_ENVELOPE_RELIABLE | LG_NET_ENVELOPE_FINAL) : + packet.envelope.flags != expectedFlags) || !lgNetEnvelopeRouteValid(&packet.envelope, LG_NET_ROLE_CLIENT, LG_NET_ROUTE_DELIVERY_RELIABLE)) return false; @@ -2980,6 +3544,12 @@ struct CNetworkServer::Impl return audio.HandlePacket(AudioPeer(connection), packet, false) != NetworkAudioPacketResult::INVALID; + case LG_NET_SERVICE_CLIPBOARD: + case LG_NET_SERVICE_FILE: + return clipboard.HandlePacket( + ClipboardPeer(connection), packet, false) != + NetworkClipboardPacketResult::INVALID; + case LG_NET_SERVICE_CONTROL: return HandleControl(connection, packet); @@ -3134,6 +3704,265 @@ struct CNetworkServer::Impl return DrainReceive(connection); } + bool ActivateBulkReceive(BulkStream& stream) + { + LGNetStreamBinding binding; + if (lgNetStreamBindingDecode(&binding, stream.bindingData, + sizeof(stream.bindingData)) != LG_NET_PARSE_OK || + binding.originRole != LG_NET_ROLE_CLIENT || + binding.flags != (LG_NET_STREAM_BINDING_UNIDIRECTIONAL | + LG_NET_STREAM_BINDING_BULK) || + binding.streamID != stream.streamID || + binding.service <= LG_NET_SERVICE_CORE || + binding.service >= NETWORK_SERVICE_COUNT || + !BulkOriginAllowed(binding.service, LG_NET_ROLE_CLIENT)) + return false; + + Connection& connection = stream.connection; + uint32_t maxPayload; + { + CSRWSharedLock guard(connection.lock); + const uint16_t serviceMask = UINT16_C(1) << binding.service; + const bool valid = !connection.closing && + connection.handshake == HandshakeState::READY && + binding.sessionEpoch == connection.sessionEpoch && + (connection.selectedServices & serviceMask) && + (connection.bulkServices & serviceMask) && + connection.selectedCapabilities + [binding.service].maxUnidirectionalStreams && + connection.servicePeerBulkStreams[binding.service] < + connection.selectedCapabilities + [binding.service].maxUnidirectionalStreams; + maxPayload = valid ? connection.selectedCapabilities + [binding.service].maxPayloadLength : 0; + } + if (!maxPayload) + return false; + + try + { + stream.receiveData.resize( + LG_NET_ENVELOPE_WIRE_SIZE + static_cast(maxPayload)); + } + catch (...) + { + return false; + } + if (!lgNetStreamParserInit(&stream.receiveParser, + stream.receiveData.data(), stream.receiveData.size(), maxPayload)) + return false; + + { + CSRWExclusiveLock guard(connection.lock); + const uint16_t serviceMask = UINT16_C(1) << binding.service; + const bool valid = !connection.closing && + connection.handshake == HandshakeState::READY && + binding.sessionEpoch == connection.sessionEpoch && + (connection.selectedServices & serviceMask) && + (connection.bulkServices & serviceMask) && + connection.selectedCapabilities + [binding.service].maxPayloadLength == maxPayload && + connection.selectedCapabilities + [binding.service].maxUnidirectionalStreams && + connection.servicePeerBulkStreams[binding.service] < + connection.selectedCapabilities + [binding.service].maxUnidirectionalStreams; + if (!valid) + return false; + + stream.service = binding.service; + stream.bound = true; + ++connection.servicePeerBulkStreams[binding.service]; + } + return true; + } + + bool HandleBulkPacket(BulkStream& stream, const LGNetPacketView& packet) + { + if (packet.envelope.service <= LG_NET_SERVICE_CORE || + packet.envelope.service >= NETWORK_SERVICE_COUNT || + !BulkOriginAllowed( + packet.envelope.service, LG_NET_ROLE_CLIENT)) + return false; + + Connection& connection = stream.connection; + { + CSRWSharedLock guard(connection.lock); + const uint16_t serviceMask = UINT16_C(1) << packet.envelope.service; + if (connection.closing || + connection.handshake != HandshakeState::READY || + !stream.bound || packet.envelope.service != stream.service || + packet.envelope.sessionEpoch != connection.sessionEpoch || + !(connection.selectedServices & serviceMask) || + !(connection.bulkServices & serviceMask) || + packet.envelope.serviceVersion != + connection.selectedCapabilities + [packet.envelope.service].versionMin || + packet.envelope.payloadLength > + connection.selectedCapabilities + [packet.envelope.service].maxPayloadLength) + return false; + } + + if (packet.envelope.headerSize != LG_NET_ENVELOPE_WIRE_SIZE || + !(packet.envelope.flags & LG_NET_ENVELOPE_FINAL) || + packet.envelope.flags & (LG_NET_ENVELOPE_RELIABLE | + LG_NET_ENVELOPE_DATAGRAM | LG_NET_ENVELOPE_EARLY_DATA) || + !lgNetEnvelopeRouteValid(&packet.envelope, LG_NET_ROLE_CLIENT, + LG_NET_ROUTE_DELIVERY_BULK)) + return false; + + switch (packet.envelope.service) + { + case LG_NET_SERVICE_CLIPBOARD: + case LG_NET_SERVICE_FILE: + return clipboard.HandlePacket( + ClipboardPeer(connection), packet, true) != + NetworkClipboardPacketResult::INVALID; + + default: + return false; + } + } + + bool DrainBulkReceive(BulkStream& stream) + { + for (;;) + { + LGNetStreamPacket streamPacket; + const LGNetStreamResult result = lgNetStreamParserNext( + &stream.receiveParser, &streamPacket, nullptr); + if (result == LG_NET_STREAM_NEED_DATA) + return true; + if (result != LG_NET_STREAM_PACKET) + return false; + + LGNetPacketView packet = {}; + packet.envelope = streamPacket.envelope; + packet.payloadLength = streamPacket.payloadLength; + packet.wireSize = streamPacket.wireSize; + if (!streamPacket.payload[1].size) + packet.payload = streamPacket.payload[0].data; + else + { + try + { + stream.receivePayload.resize(streamPacket.payloadLength); + } + catch (...) + { + return false; + } + memcpy(stream.receivePayload.data(), streamPacket.payload[0].data, + streamPacket.payload[0].size); + memcpy(stream.receivePayload.data() + streamPacket.payload[0].size, + streamPacket.payload[1].data, streamPacket.payload[1].size); + packet.payload = stream.receivePayload.data(); + } + + { + CSRWExclusiveLock guard(stream.connection.lock); + if (!stream.bound || !packet.envelope.sequence || + stream.receiveSequence == UINT64_MAX || + packet.envelope.sequence != stream.receiveSequence + 1) + return false; + stream.receiveSequence = packet.envelope.sequence; + } + if (!HandleBulkPacket(stream, packet) || + !lgNetStreamParserConsume(&stream.receiveParser)) + return false; + + if (!lgNetStreamParserBuffered(&stream.receiveParser)) + lgNetStreamParserReset(&stream.receiveParser); + } + } + + bool ReceiveBulk(BulkStream& stream, const QUIC_BUFFER * buffers, + uint32_t count) + { + if (count && !buffers) + return false; + + for (uint32_t index = 0; index < count; ++index) + { + if (buffers[index].Length && !buffers[index].Buffer) + return false; + + size_t offset = 0; + while (offset < buffers[index].Length) + { + if (!stream.bound) + { + const size_t remaining = buffers[index].Length - offset; + size_t length = sizeof(stream.bindingData) - + stream.bindingLength; + if (length > remaining) + length = remaining; + memcpy(stream.bindingData + stream.bindingLength, + buffers[index].Buffer + offset, length); + stream.bindingLength += length; + offset += length; + if (stream.bindingLength == sizeof(stream.bindingData) && + !ActivateBulkReceive(stream)) + return false; + continue; + } + + size_t accepted; + if (!lgNetStreamParserAppend(&stream.receiveParser, + buffers[index].Buffer + offset, + buffers[index].Length - offset, &accepted)) + return false; + offset += accepted; + if (!DrainBulkReceive(stream) || + (!accepted && + !lgNetStreamParserWritable(&stream.receiveParser))) + return false; + } + } + + return !stream.bound || DrainBulkReceive(stream); + } + + bool AcceptBulkStream(Connection& connection, HQUIC handle, + QUIC_STREAM_OPEN_FLAGS flags) + { + if (flags != QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL) + return false; + + QUIC_UINT62 streamID; + uint32_t streamIDSize = sizeof(streamID); + if (QUIC_FAILED(api->GetParam(handle, QUIC_PARAM_STREAM_ID, + &streamIDSize, &streamID)) || + streamIDSize != sizeof(streamID) || (streamID & 3U) != 2U) + return false; + + std::unique_ptr stream( + new (std::nothrow) BulkStream(connection)); + if (!stream) + return false; + stream->handle = handle; + stream->streamID = streamID; + + { + CSRWExclusiveLock guard(connection.lock); + if (connection.closing || + connection.handshake != HandshakeState::READY || + !connection.maxPeerBulkStreams || + connection.peerBulkStreams >= connection.maxPeerBulkStreams) + return false; + + stream->next = connection.inboundBulk; + connection.inboundBulk = stream.get(); + ++connection.peerBulkStreams; + } + + api->SetCallbackHandler(handle, + reinterpret_cast(BulkStreamCallback), stream.get()); + stream.release(); + return true; + } + void ConnectionClosed(Connection& connection) { const uint32_t client = connection.client; @@ -3150,11 +3979,22 @@ struct CNetworkServer::Impl { CSRWExclusiveLock guard(connection.lock); if (connection.reliableSend.count || connection.reliableSend.bytes || - connection.datagramSend.count || connection.datagramSend.bytes) + connection.datagramSend.count || connection.datagramSend.bytes || + connection.bulkSend.count || connection.bulkSend.bytes) DEBUG_ERROR("Network client %u closed with sends in flight", connection.client); + if (connection.inboundBulk) + DEBUG_ERROR("Network client %u closed with inbound bulk streams", + connection.client); + for (LGNetService service = LG_NET_SERVICE_CORE; + service < NETWORK_SERVICE_COUNT; ++service) + if (connection.outboundBulk[service]) + DEBUG_ERROR("Network client %u closed with a bulk stream for " + "service %u", connection.client, + static_cast(service)); session = std::move(connection.session); } + clipboard.PeerDisconnected(ClipboardPeer(connection)); audio.PeerDisconnected(AudioPeer(connection)); if (session) transport.RemoveSession(client, generation); @@ -3253,14 +4093,30 @@ struct CNetworkServer::Impl case QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED: { - const bool invalidFlags = - event->PEER_STREAM_STARTED.Flags & - (QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL | - QUIC_STREAM_OPEN_FLAG_0_RTT); + const QUIC_STREAM_OPEN_FLAGS flags = + event->PEER_STREAM_STARTED.Flags; + if (flags & QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL) + { + if (!owner.AcceptBulkStream(connection, + event->PEER_STREAM_STARTED.Stream, flags)) + { + owner.api->SetCallbackHandler( + event->PEER_STREAM_STARTED.Stream, + reinterpret_cast(RejectedStreamCallback), &owner); + owner.api->StreamShutdown(event->PEER_STREAM_STARTED.Stream, + QUIC_STREAM_SHUTDOWN_FLAG_ABORT | + QUIC_STREAM_SHUTDOWN_FLAG_IMMEDIATE, + NETWORK_ERROR_PROTOCOL); + owner.Shutdown(connection, NETWORK_ERROR_PROTOCOL); + } + return QUIC_STATUS_SUCCESS; + } + bool accepted = false; { CSRWExclusiveLock guard(connection.lock); - if (!invalidFlags && !connection.closing && + if (flags == QUIC_STREAM_OPEN_FLAG_NONE && + !connection.closing && !connection.controlStream) { connection.controlStream = event->PEER_STREAM_STARTED.Stream; @@ -3297,7 +4153,7 @@ struct CNetworkServer::Impl connection.maxDatagramSendLength = event->DATAGRAM_STATE_CHANGED.MaxSendLength; connection.datagramSend.writableServices |= - ReadyServicesLocked(connection, true); + ReadyServicesLocked(connection, Delivery::DATAGRAM); } owner.NotifyAudioSendReady(connection, true); return QUIC_STATUS_SUCCESS; @@ -3359,6 +4215,7 @@ struct CNetworkServer::Impl ReleaseSendLocked(connection, *send); } owner.NotifyAudioSendReady(connection, false); + owner.NotifyClipboardSendReady(connection, Delivery::RELIABLE); return QUIC_STATUS_SUCCESS; } @@ -3384,6 +4241,189 @@ struct CNetworkServer::Impl } } + static QUIC_STATUS QUIC_API BulkStreamCallback(HQUIC handle, + void * context, QUIC_STREAM_EVENT * event) + { + BulkStream& stream = *static_cast(context); + Connection& connection = stream.connection; + Impl& owner = connection.owner; + CallbackGuard callback(owner); + + switch (event->Type) + { + case QUIC_STREAM_EVENT_START_COMPLETE: + { + if (QUIC_FAILED(event->START_COMPLETE.Status)) + { + bool deferFree; + { + CSRWExclusiveLock guard(connection.lock); + DetachBulkStreamLocked(stream); + deferFree = stream.outbound && + !stream.openingReturned; + stream.shutdownComplete = true; + } + owner.api->StreamClose(handle); + if (!deferFree) + delete &stream; + owner.Shutdown(connection, NETWORK_ERROR_PROTOCOL); + return QUIC_STATUS_SUCCESS; + } + if (!stream.outbound || !event->START_COMPLETE.ID || + (event->START_COMPLETE.ID & 3U) != 3U) + { + owner.api->StreamShutdown(handle, + QUIC_STREAM_SHUTDOWN_FLAG_ABORT | + QUIC_STREAM_SHUTDOWN_FLAG_IMMEDIATE, + NETWORK_ERROR_PROTOCOL); + owner.Shutdown(connection, NETWORK_ERROR_PROTOCOL); + return QUIC_STATUS_SUCCESS; + } + + uint64_t sessionEpoch; + { + CSRWSharedLock guard(connection.lock); + const uint16_t serviceMask = UINT16_C(1) << stream.service; + if (connection.closing || + connection.handshake != HandshakeState::READY || + connection.outboundBulk[stream.service] != &stream || + !(connection.selectedServices & serviceMask) || + !(connection.bulkServices & serviceMask)) + { + owner.api->StreamShutdown(handle, + QUIC_STREAM_SHUTDOWN_FLAG_ABORT | + QUIC_STREAM_SHUTDOWN_FLAG_IMMEDIATE, + NETWORK_ERROR_PROTOCOL); + return QUIC_STATUS_SUCCESS; + } + sessionEpoch = connection.sessionEpoch; + } + + LGNetStreamBinding binding; + lgNetStreamBindingInit(&binding, stream.service, LG_NET_ROLE_SERVER, + event->START_COMPLETE.ID, sessionEpoch, + LG_NET_STREAM_BINDING_UNIDIRECTIONAL | + LG_NET_STREAM_BINDING_BULK); + std::unique_ptr send(new (std::nothrow) Send()); + if (!send) + { + owner.Shutdown(connection, NETWORK_ERROR_PROTOCOL); + return QUIC_STATUS_SUCCESS; + } + try + { + send->data.resize(LG_NET_STREAM_BINDING_WIRE_SIZE); + } + catch (...) + { + owner.Shutdown(connection, NETWORK_ERROR_PROTOCOL); + return QUIC_STATUS_SUCCESS; + } + if (!lgNetStreamBindingEncode(send->data.data(), send->data.size(), + &binding)) + { + owner.Shutdown(connection, NETWORK_ERROR_PROTOCOL); + return QUIC_STATUS_SUCCESS; + } + + send->buffer.Buffer = send->data.data(); + send->buffer.Length = LG_NET_STREAM_BINDING_WIRE_SIZE; + send->service = stream.service; + send->length = LG_NET_STREAM_BINDING_WIRE_SIZE; + send->delivery = Delivery::BULK; + send->accounted = false; + + uint16_t ready; + QUIC_STATUS status; + { + CSRWExclusiveLock guard(connection.lock); + const uint16_t serviceMask = UINT16_C(1) << stream.service; + const bool valid = !connection.closing && + connection.handshake == HandshakeState::READY && + connection.sessionEpoch == sessionEpoch && + connection.outboundBulk[stream.service] == &stream && + (connection.selectedServices & serviceMask) && + (connection.bulkServices & serviceMask); + status = valid ? owner.api->StreamSend(handle, &send->buffer, 1, + QUIC_SEND_FLAG_NONE, send.get()) : QUIC_STATUS_ABORTED; + ready = 0; + if (QUIC_SUCCEEDED(status)) + { + stream.streamID = event->START_COMPLETE.ID; + stream.bound = true; + if (stream.openingReturned) + ready = ReadyServicesLocked(connection, Delivery::BULK); + } + } + if (QUIC_FAILED(status)) + { + owner.api->StreamShutdown(handle, + QUIC_STREAM_SHUTDOWN_FLAG_ABORT | + QUIC_STREAM_SHUTDOWN_FLAG_IMMEDIATE, + NETWORK_ERROR_PROTOCOL); + owner.Shutdown(connection, NETWORK_ERROR_PROTOCOL); + return QUIC_STATUS_SUCCESS; + } + + send.release(); + if (ready) + owner.NotifyClipboardSendReady(connection, Delivery::BULK); + return QUIC_STATUS_SUCCESS; + } + + case QUIC_STREAM_EVENT_RECEIVE: + if (stream.outbound || !owner.ReceiveBulk(stream, + event->RECEIVE.Buffers, event->RECEIVE.BufferCount)) + owner.Shutdown(connection, NETWORK_ERROR_PROTOCOL); + return QUIC_STATUS_SUCCESS; + + case QUIC_STREAM_EVENT_SEND_COMPLETE: + { + std::unique_ptr send( + static_cast(event->SEND_COMPLETE.ClientContext)); + { + CSRWExclusiveLock guard(connection.lock); + ReleaseSendLocked(connection, *send); + } + owner.NotifyClipboardSendReady(connection, Delivery::BULK); + return QUIC_STATUS_SUCCESS; + } + + case QUIC_STREAM_EVENT_PEER_SEND_SHUTDOWN: + case QUIC_STREAM_EVENT_PEER_SEND_ABORTED: + case QUIC_STREAM_EVENT_PEER_RECEIVE_ABORTED: + { + bool closing; + { + CSRWSharedLock guard(connection.lock); + closing = connection.closing; + } + if (!closing) + owner.Shutdown(connection, NETWORK_ERROR_PROTOCOL); + return QUIC_STATUS_SUCCESS; + } + + case QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE: + { + bool deferFree; + { + CSRWExclusiveLock guard(connection.lock); + DetachBulkStreamLocked(stream); + deferFree = stream.outbound && + !stream.openingReturned; + stream.shutdownComplete = true; + } + owner.api->StreamClose(handle); + if (!deferFree) + delete &stream; + return QUIC_STATUS_SUCCESS; + } + + default: + return QUIC_STATUS_SUCCESS; + } + } + static QUIC_STATUS QUIC_API RejectedStreamCallback(HQUIC handle, void * context, QUIC_STREAM_EVENT * event) { @@ -3398,6 +4438,7 @@ struct CNetworkServer::Impl { const uint64_t now = GetTickCount64(); audio.Process(now); + clipboard.Process(now); CSRWSharedLock guard(lock); if (stopping || !api) return; @@ -3624,8 +4665,9 @@ struct CNetworkServer::Impl }; CNetworkServer::CNetworkServer(CNetworkTransport& transport, - CNetworkAudio& audio, const NetworkSettings& settings) : - m_impl(new Impl(transport, audio, settings)) + CNetworkAudio& audio, CNetworkClipboard& clipboard, + const NetworkSettings& settings) : + m_impl(new Impl(transport, audio, clipboard, settings)) { } @@ -3665,3 +4707,21 @@ NetworkAudioSendResult CNetworkServer::SendAudio( componentEpoch, requestID, payload, payloadLength) : NetworkAudioSendResult::FAILED; } + +bool CNetworkServer::ClipboardPeerInfo( + const NetworkClipboardPeer& peer, NetworkClipboardPeerInfo& info) +{ + return m_impl && m_impl->ClipboardPeerInfo(peer, info); +} + +NetworkClipboardSendResult CNetworkServer::SendClipboard( + const NetworkClipboardPeer& peer, bool bulk, LGNetService service, + uint16_t messageType, LGNetEnvelopeFlags flags, + uint64_t componentEpoch, uint64_t requestID, + const void * payload, uint32_t payloadLength) +{ + return m_impl ? + m_impl->SendClipboard(peer, bulk, service, messageType, flags, + componentEpoch, requestID, payload, payloadLength) : + NetworkClipboardSendResult::FAILED; +} diff --git a/idd/LGIdd/transport/network/CNetworkServer.h b/idd/LGIdd/transport/network/CNetworkServer.h index 94a83fcd..4aa4153b 100644 --- a/idd/LGIdd/transport/network/CNetworkServer.h +++ b/idd/LGIdd/transport/network/CNetworkServer.h @@ -21,13 +21,16 @@ #pragma once #include "transport/network/CNetworkAudio.h" +#include "transport/network/CNetworkClipboard.h" #include class CNetworkTransport; struct NetworkSettings; -class CNetworkServer final : public INetworkAudioSender +class CNetworkServer final : + public INetworkAudioSender, + public INetworkClipboardSender { private: struct Impl; @@ -35,7 +38,7 @@ private: public: CNetworkServer(CNetworkTransport& transport, CNetworkAudio& audio, - const NetworkSettings& settings); + CNetworkClipboard& clipboard, const NetworkSettings& settings); ~CNetworkServer(); CNetworkServer(const CNetworkServer&) = delete; @@ -52,4 +55,12 @@ public: uint16_t messageType, LGNetEnvelopeFlags flags, uint64_t componentEpoch, uint64_t requestID, const void * payload, uint32_t payloadLength) override; + + bool ClipboardPeerInfo(const NetworkClipboardPeer& peer, + NetworkClipboardPeerInfo& info) override; + NetworkClipboardSendResult SendClipboard( + const NetworkClipboardPeer& peer, bool bulk, LGNetService service, + uint16_t messageType, LGNetEnvelopeFlags flags, + uint64_t componentEpoch, uint64_t requestID, + const void * payload, uint32_t payloadLength) override; }; diff --git a/idd/LGIdd/transport/network/CNetworkTransport.cpp b/idd/LGIdd/transport/network/CNetworkTransport.cpp index 227f1375..3a9159ae 100644 --- a/idd/LGIdd/transport/network/CNetworkTransport.cpp +++ b/idd/LGIdd/transport/network/CNetworkTransport.cpp @@ -76,7 +76,8 @@ bool CNetworkTransport::Initialize() try { - m_server.reset(new CNetworkServer(*this, m_audio, m_settings)); + m_server.reset( + new CNetworkServer(*this, m_audio, m_clipboard, m_settings)); } catch (...) { @@ -89,6 +90,13 @@ bool CNetworkTransport::Initialize() m_server.reset(); return false; } + if (!m_clipboard.AttachSender(*m_server)) + { + DEBUG_ERROR("Failed to attach the network clipboard sender"); + m_audio.DetachSender(); + m_server.reset(); + return false; + } m_state = State::INITIALIZED; if (m_config.services & TRANSPORT_SERVICE_FRAME) @@ -163,6 +171,7 @@ void CNetworkTransport::Stop() if (server) server->Stop(); + m_clipboard.DetachSender(); m_audio.DetachSender(); std::vector sessions; @@ -175,6 +184,7 @@ void CNetworkTransport::Stop() m_input.Stop(); m_audio.Stop(); + m_clipboard.Stop(); m_control.SetControlEvents(nullptr, {}); m_events.Reset(); } @@ -204,13 +214,13 @@ const FrameProfile * CNetworkTransport::Profiles(unsigned& count) const CfgResult CNetworkTransport::Probe(const FrameCfg& cfg) const { - UNREFERENCED_PARAMETER(cfg); + (void)cfg; return CfgResult::REJECTED; } CfgResult CNetworkTransport::Prepare(const FrameCfg& cfg) { - UNREFERENCED_PARAMETER(cfg); + (void)cfg; return CfgResult::REJECTED; } @@ -237,6 +247,12 @@ IAudioSource * CNetworkTransport::Audio() return (m_config.services & TRANSPORT_SERVICE_AUDIO) ? &m_audio : nullptr; } +IClipboardSource * CNetworkTransport::Clipboard() +{ + return (m_config.services & TRANSPORT_SERVICE_CLIPBOARD) ? + &m_clipboard : nullptr; +} + IControlSink * CNetworkTransport::Control() { return (m_config.services & TRANSPORT_SERVICE_CONTROL) ? diff --git a/idd/LGIdd/transport/network/CNetworkTransport.h b/idd/LGIdd/transport/network/CNetworkTransport.h index d73d39db..0fc8b087 100644 --- a/idd/LGIdd/transport/network/CNetworkTransport.h +++ b/idd/LGIdd/transport/network/CNetworkTransport.h @@ -23,6 +23,7 @@ #include "CSRWLock.h" #include "transport/ITransport.h" #include "transport/network/CNetworkAudio.h" +#include "transport/network/CNetworkClipboard.h" #include "transport/network/CNetworkSession.h" #include "transport/network/NetworkSettings.h" @@ -51,6 +52,7 @@ private: TransportInstance m_config; NetworkSettings m_settings; CNetworkAudio m_audio; + CNetworkClipboard m_clipboard; std::unique_ptr m_server; mutable CSRWLock m_lifecycleLock; @@ -92,6 +94,7 @@ public: std::shared_ptr GetFrameCaps() const override; DirectFrameBufferMemory GetDirectMemory() const override; IAudioSource * Audio() override; + IClipboardSource * Clipboard() override; IControlSink * Control() override; IInputSource * Input() override; diff --git a/repos/LGProtocol b/repos/LGProtocol index 5eec26f3..a2e35d67 160000 --- a/repos/LGProtocol +++ b/repos/LGProtocol @@ -1 +1 @@ -Subproject commit 5eec26f3cc955024a54305ef56d9c7c69281741a +Subproject commit a2e35d67ce0baea91202ed483b58ed0ca1ad50c5