mirror of
https://github.com/kasmtech/KasmVNC.git
synced 2024-11-23 08:33:22 +01:00
648 lines
18 KiB
C
648 lines
18 KiB
C
/* Copyright 2016-2019 Pierre Ossman for Cendio AB
|
|
*
|
|
* This 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 software 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 software; if not, write to the Free Software
|
|
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
|
|
* USA.
|
|
*/
|
|
|
|
#ifdef HAVE_DIX_CONFIG_H
|
|
#include <dix-config.h>
|
|
#endif
|
|
|
|
#include <X11/Xatom.h>
|
|
|
|
#include "propertyst.h"
|
|
#include "scrnintstr.h"
|
|
#include "selection.h"
|
|
#include "windowstr.h"
|
|
#include "xace.h"
|
|
|
|
#include "xorg-version.h"
|
|
|
|
#include "vncExtInit.h"
|
|
#include "vncSelection.h"
|
|
#include "RFBGlue.h"
|
|
|
|
#define LOG_NAME "Selection"
|
|
|
|
#define LOG_ERROR(...) vncLogError(LOG_NAME, __VA_ARGS__)
|
|
#define LOG_STATUS(...) vncLogStatus(LOG_NAME, __VA_ARGS__)
|
|
#define LOG_INFO(...) vncLogInfo(LOG_NAME, __VA_ARGS__)
|
|
#define LOG_DEBUG(...) vncLogDebug(LOG_NAME, __VA_ARGS__)
|
|
|
|
static Atom xaPRIMARY, xaCLIPBOARD;
|
|
static Atom xaTARGETS, xaTIMESTAMP, xaSTRING, xaTEXT, xaUTF8_STRING;
|
|
|
|
static WindowPtr pWindow;
|
|
static Window wid;
|
|
|
|
static Bool probing;
|
|
static Atom activeSelection = None;
|
|
|
|
struct VncDataTarget {
|
|
ClientPtr client;
|
|
Atom selection;
|
|
Atom target;
|
|
Atom property;
|
|
Window requestor;
|
|
CARD32 time;
|
|
struct VncDataTarget* next;
|
|
};
|
|
|
|
static struct VncDataTarget* vncDataTargetHead;
|
|
|
|
static int vncCreateSelectionWindow(void);
|
|
static int vncOwnSelection(Atom selection);
|
|
static int vncConvertSelection(ClientPtr client, Atom selection,
|
|
Atom target, Atom property,
|
|
Window requestor, CARD32 time,
|
|
const char* data);
|
|
static int vncProcConvertSelection(ClientPtr client);
|
|
static void vncSelectionRequest(Atom selection, Atom target);
|
|
static int vncProcSendEvent(ClientPtr client);
|
|
static void vncSelectionCallback(CallbackListPtr *callbacks,
|
|
void * data, void * args);
|
|
static void vncClientStateCallback(CallbackListPtr * l,
|
|
void * d, void * p);
|
|
|
|
static int (*origProcConvertSelection)(ClientPtr);
|
|
static int (*origProcSendEvent)(ClientPtr);
|
|
|
|
void vncSelectionInit(void)
|
|
{
|
|
xaPRIMARY = MakeAtom("PRIMARY", 7, TRUE);
|
|
xaCLIPBOARD = MakeAtom("CLIPBOARD", 9, TRUE);
|
|
|
|
xaTARGETS = MakeAtom("TARGETS", 7, TRUE);
|
|
xaTIMESTAMP = MakeAtom("TIMESTAMP", 9, TRUE);
|
|
xaSTRING = MakeAtom("STRING", 6, TRUE);
|
|
xaTEXT = MakeAtom("TEXT", 4, TRUE);
|
|
xaUTF8_STRING = MakeAtom("UTF8_STRING", 11, TRUE);
|
|
|
|
/* There are no hooks for when these are internal windows, so
|
|
* override the relevant handlers. */
|
|
origProcConvertSelection = ProcVector[X_ConvertSelection];
|
|
ProcVector[X_ConvertSelection] = vncProcConvertSelection;
|
|
origProcSendEvent = ProcVector[X_SendEvent];
|
|
ProcVector[X_SendEvent] = vncProcSendEvent;
|
|
|
|
if (!AddCallback(&SelectionCallback, vncSelectionCallback, 0))
|
|
FatalError("Add VNC SelectionCallback failed\n");
|
|
if (!AddCallback(&ClientStateCallback, vncClientStateCallback, 0))
|
|
FatalError("Add VNC ClientStateCallback failed\n");
|
|
}
|
|
|
|
void vncHandleClipboardRequest(void)
|
|
{
|
|
if (activeSelection == None) {
|
|
LOG_DEBUG("Got request for local clipboard although no clipboard is active");
|
|
return;
|
|
}
|
|
|
|
LOG_DEBUG("Got request for local clipboard, re-probing formats");
|
|
|
|
probing = FALSE;
|
|
vncSelectionRequest(activeSelection, xaTARGETS);
|
|
}
|
|
|
|
void vncHandleClipboardAnnounce(int available)
|
|
{
|
|
if (available) {
|
|
int rc;
|
|
|
|
LOG_DEBUG("Remote clipboard announced, grabbing local ownership");
|
|
|
|
if (vncGetSetPrimary()) {
|
|
rc = vncOwnSelection(xaPRIMARY);
|
|
if (rc != Success)
|
|
LOG_ERROR("Could not set PRIMARY selection");
|
|
}
|
|
|
|
rc = vncOwnSelection(xaCLIPBOARD);
|
|
if (rc != Success)
|
|
LOG_ERROR("Could not set CLIPBOARD selection");
|
|
} else {
|
|
struct VncDataTarget* next;
|
|
|
|
if (pWindow == NULL)
|
|
return;
|
|
|
|
LOG_DEBUG("Remote clipboard lost, removing local ownership");
|
|
|
|
DeleteWindowFromAnySelections(pWindow);
|
|
|
|
/* Abort any pending transfer */
|
|
while (vncDataTargetHead != NULL) {
|
|
xEvent event;
|
|
|
|
event.u.u.type = SelectionNotify;
|
|
event.u.selectionNotify.time = vncDataTargetHead->time;
|
|
event.u.selectionNotify.requestor = vncDataTargetHead->requestor;
|
|
event.u.selectionNotify.selection = vncDataTargetHead->selection;
|
|
event.u.selectionNotify.target = vncDataTargetHead->target;
|
|
event.u.selectionNotify.property = None;
|
|
WriteEventsToClient(vncDataTargetHead->client, 1, &event);
|
|
|
|
next = vncDataTargetHead->next;
|
|
free(vncDataTargetHead);
|
|
vncDataTargetHead = next;
|
|
}
|
|
}
|
|
}
|
|
|
|
void vncHandleClipboardData(const char* data)
|
|
{
|
|
struct VncDataTarget* next;
|
|
|
|
LOG_DEBUG("Got remote clipboard data, sending to X11 clients");
|
|
|
|
while (vncDataTargetHead != NULL) {
|
|
int rc;
|
|
xEvent event;
|
|
|
|
rc = vncConvertSelection(vncDataTargetHead->client,
|
|
vncDataTargetHead->selection,
|
|
vncDataTargetHead->target,
|
|
vncDataTargetHead->property,
|
|
vncDataTargetHead->requestor,
|
|
vncDataTargetHead->time,
|
|
data);
|
|
if (rc != Success) {
|
|
event.u.u.type = SelectionNotify;
|
|
event.u.selectionNotify.time = vncDataTargetHead->time;
|
|
event.u.selectionNotify.requestor = vncDataTargetHead->requestor;
|
|
event.u.selectionNotify.selection = vncDataTargetHead->selection;
|
|
event.u.selectionNotify.target = vncDataTargetHead->target;
|
|
event.u.selectionNotify.property = None;
|
|
WriteEventsToClient(vncDataTargetHead->client, 1, &event);
|
|
}
|
|
|
|
next = vncDataTargetHead->next;
|
|
free(vncDataTargetHead);
|
|
vncDataTargetHead = next;
|
|
}
|
|
}
|
|
|
|
static int vncCreateSelectionWindow(void)
|
|
{
|
|
ScreenPtr pScreen;
|
|
int result;
|
|
|
|
if (pWindow != NULL)
|
|
return Success;
|
|
|
|
pScreen = screenInfo.screens[0];
|
|
|
|
wid = FakeClientID(0);
|
|
pWindow = CreateWindow(wid, pScreen->root,
|
|
0, 0, 100, 100, 0, InputOnly,
|
|
0, NULL, 0, serverClient,
|
|
CopyFromParent, &result);
|
|
if (!pWindow)
|
|
return result;
|
|
|
|
if (!AddResource(pWindow->drawable.id, RT_WINDOW, pWindow))
|
|
return BadAlloc;
|
|
|
|
LOG_DEBUG("Created selection window");
|
|
|
|
return Success;
|
|
}
|
|
|
|
static int vncOwnSelection(Atom selection)
|
|
{
|
|
Selection *pSel;
|
|
int rc;
|
|
|
|
SelectionInfoRec info;
|
|
|
|
rc = vncCreateSelectionWindow();
|
|
if (rc != Success)
|
|
return rc;
|
|
|
|
rc = dixLookupSelection(&pSel, selection, serverClient, DixSetAttrAccess);
|
|
if (rc == Success) {
|
|
if (pSel->client && (pSel->client != serverClient)) {
|
|
xEvent event = {
|
|
.u.selectionClear.time = currentTime.milliseconds,
|
|
.u.selectionClear.window = pSel->window,
|
|
.u.selectionClear.atom = pSel->selection
|
|
};
|
|
event.u.u.type = SelectionClear;
|
|
WriteEventsToClient(pSel->client, 1, &event);
|
|
}
|
|
} else if (rc == BadMatch) {
|
|
pSel = dixAllocateObjectWithPrivates(Selection, PRIVATE_SELECTION);
|
|
if (!pSel)
|
|
return BadAlloc;
|
|
|
|
pSel->selection = selection;
|
|
|
|
rc = XaceHookSelectionAccess(serverClient, &pSel,
|
|
DixCreateAccess | DixSetAttrAccess);
|
|
if (rc != Success) {
|
|
free(pSel);
|
|
return rc;
|
|
}
|
|
|
|
pSel->next = CurrentSelections;
|
|
CurrentSelections = pSel;
|
|
}
|
|
else
|
|
return rc;
|
|
|
|
pSel->lastTimeChanged = currentTime;
|
|
pSel->window = wid;
|
|
pSel->pWin = pWindow;
|
|
pSel->client = serverClient;
|
|
|
|
LOG_DEBUG("Grabbed %s selection", NameForAtom(selection));
|
|
|
|
info.selection = pSel;
|
|
info.client = serverClient;
|
|
info.kind = SelectionSetOwner;
|
|
CallCallbacks(&SelectionCallback, &info);
|
|
|
|
return Success;
|
|
}
|
|
|
|
static int vncConvertSelection(ClientPtr client, Atom selection,
|
|
Atom target, Atom property,
|
|
Window requestor, CARD32 time,
|
|
const char* data)
|
|
{
|
|
Selection *pSel;
|
|
WindowPtr pWin;
|
|
int rc;
|
|
|
|
Atom realProperty;
|
|
|
|
xEvent event;
|
|
|
|
if (data == NULL) {
|
|
LOG_DEBUG("Selection request for %s (type %s)",
|
|
NameForAtom(selection), NameForAtom(target));
|
|
} else {
|
|
LOG_DEBUG("Sending data for selection request for %s (type %s)",
|
|
NameForAtom(selection), NameForAtom(target));
|
|
}
|
|
|
|
rc = dixLookupSelection(&pSel, selection, client, DixGetAttrAccess);
|
|
if (rc != Success)
|
|
return rc;
|
|
|
|
/* We do not validate the time argument because neither does
|
|
* dix/selection.c and some clients (e.g. Qt) relies on this */
|
|
|
|
rc = dixLookupWindow(&pWin, requestor, client, DixSetAttrAccess);
|
|
if (rc != Success)
|
|
return rc;
|
|
|
|
if (property != None)
|
|
realProperty = property;
|
|
else
|
|
realProperty = target;
|
|
|
|
/* FIXME: MULTIPLE target */
|
|
|
|
if (target == xaTARGETS) {
|
|
Atom targets[] = { xaTARGETS, xaTIMESTAMP,
|
|
xaSTRING, xaTEXT, xaUTF8_STRING };
|
|
|
|
rc = dixChangeWindowProperty(serverClient, pWin, realProperty,
|
|
XA_ATOM, 32, PropModeReplace,
|
|
sizeof(targets)/sizeof(targets[0]),
|
|
targets, TRUE);
|
|
if (rc != Success)
|
|
return rc;
|
|
} else if (target == xaTIMESTAMP) {
|
|
rc = dixChangeWindowProperty(serverClient, pWin, realProperty,
|
|
XA_INTEGER, 32, PropModeReplace, 1,
|
|
&pSel->lastTimeChanged.milliseconds,
|
|
TRUE);
|
|
if (rc != Success)
|
|
return rc;
|
|
} else {
|
|
if (data == NULL) {
|
|
struct VncDataTarget* vdt;
|
|
|
|
if ((target != xaSTRING) && (target != xaTEXT) &&
|
|
(target != xaUTF8_STRING))
|
|
return BadMatch;
|
|
|
|
vdt = calloc(1, sizeof(struct VncDataTarget));
|
|
if (vdt == NULL)
|
|
return BadAlloc;
|
|
|
|
vdt->client = client;
|
|
vdt->selection = selection;
|
|
vdt->target = target;
|
|
vdt->property = property;
|
|
vdt->requestor = requestor;
|
|
vdt->time = time;
|
|
|
|
vdt->next = vncDataTargetHead;
|
|
vncDataTargetHead = vdt;
|
|
|
|
LOG_DEBUG("Requesting clipboard data from client");
|
|
|
|
vncRequestClipboard();
|
|
|
|
return Success;
|
|
} else {
|
|
if ((target == xaSTRING) || (target == xaTEXT)) {
|
|
char* latin1;
|
|
|
|
latin1 = vncUTF8ToLatin1(data, (size_t)-1);
|
|
if (latin1 == NULL)
|
|
return BadAlloc;
|
|
|
|
rc = dixChangeWindowProperty(serverClient, pWin, realProperty,
|
|
XA_STRING, 8, PropModeReplace,
|
|
strlen(latin1), latin1, TRUE);
|
|
|
|
vncStrFree(latin1);
|
|
|
|
if (rc != Success)
|
|
return rc;
|
|
} else if (target == xaUTF8_STRING) {
|
|
rc = dixChangeWindowProperty(serverClient, pWin, realProperty,
|
|
xaUTF8_STRING, 8, PropModeReplace,
|
|
strlen(data), data, TRUE);
|
|
if (rc != Success)
|
|
return rc;
|
|
} else {
|
|
return BadMatch;
|
|
}
|
|
}
|
|
}
|
|
|
|
event.u.u.type = SelectionNotify;
|
|
event.u.selectionNotify.time = time;
|
|
event.u.selectionNotify.requestor = requestor;
|
|
event.u.selectionNotify.selection = selection;
|
|
event.u.selectionNotify.target = target;
|
|
event.u.selectionNotify.property = property;
|
|
WriteEventsToClient(client, 1, &event);
|
|
return Success;
|
|
}
|
|
|
|
static int vncProcConvertSelection(ClientPtr client)
|
|
{
|
|
Bool paramsOkay;
|
|
WindowPtr pWin;
|
|
Selection *pSel;
|
|
int rc;
|
|
|
|
REQUEST(xConvertSelectionReq);
|
|
REQUEST_SIZE_MATCH(xConvertSelectionReq);
|
|
|
|
rc = dixLookupWindow(&pWin, stuff->requestor, client, DixSetAttrAccess);
|
|
if (rc != Success)
|
|
return rc;
|
|
|
|
paramsOkay = ValidAtom(stuff->selection) && ValidAtom(stuff->target);
|
|
paramsOkay &= (stuff->property == None) || ValidAtom(stuff->property);
|
|
if (!paramsOkay) {
|
|
client->errorValue = stuff->property;
|
|
return BadAtom;
|
|
}
|
|
|
|
rc = dixLookupSelection(&pSel, stuff->selection, client, DixReadAccess);
|
|
if (rc == Success && pSel->client == serverClient &&
|
|
pSel->window == wid) {
|
|
rc = vncConvertSelection(client, stuff->selection,
|
|
stuff->target, stuff->property,
|
|
stuff->requestor, stuff->time, NULL);
|
|
if (rc != Success) {
|
|
xEvent event;
|
|
|
|
memset(&event, 0, sizeof(xEvent));
|
|
event.u.u.type = SelectionNotify;
|
|
event.u.selectionNotify.time = stuff->time;
|
|
event.u.selectionNotify.requestor = stuff->requestor;
|
|
event.u.selectionNotify.selection = stuff->selection;
|
|
event.u.selectionNotify.target = stuff->target;
|
|
event.u.selectionNotify.property = None;
|
|
WriteEventsToClient(client, 1, &event);
|
|
}
|
|
|
|
return Success;
|
|
}
|
|
|
|
return origProcConvertSelection(client);
|
|
}
|
|
|
|
static void vncSelectionRequest(Atom selection, Atom target)
|
|
{
|
|
Selection *pSel;
|
|
xEvent event;
|
|
int rc;
|
|
|
|
rc = vncCreateSelectionWindow();
|
|
if (rc != Success)
|
|
return;
|
|
|
|
LOG_DEBUG("Requesting %s for %s selection",
|
|
NameForAtom(target), NameForAtom(selection));
|
|
|
|
rc = dixLookupSelection(&pSel, selection, serverClient, DixGetAttrAccess);
|
|
if (rc != Success)
|
|
return;
|
|
|
|
event.u.u.type = SelectionRequest;
|
|
event.u.selectionRequest.owner = pSel->window;
|
|
event.u.selectionRequest.time = currentTime.milliseconds;
|
|
event.u.selectionRequest.requestor = wid;
|
|
event.u.selectionRequest.selection = selection;
|
|
event.u.selectionRequest.target = target;
|
|
event.u.selectionRequest.property = target;
|
|
WriteEventsToClient(pSel->client, 1, &event);
|
|
}
|
|
|
|
static Bool vncHasAtom(Atom atom, const Atom list[], size_t size)
|
|
{
|
|
size_t i;
|
|
|
|
for (i = 0;i < size;i++) {
|
|
if (list[i] == atom)
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static void vncHandleSelection(Atom selection, Atom target,
|
|
Atom property, Atom requestor,
|
|
TimeStamp time)
|
|
{
|
|
PropertyPtr prop;
|
|
int rc;
|
|
|
|
rc = dixLookupProperty(&prop, pWindow, property,
|
|
serverClient, DixReadAccess);
|
|
if (rc != Success)
|
|
return;
|
|
|
|
LOG_DEBUG("Selection notification for %s (target %s, property %s, type %s)",
|
|
NameForAtom(selection), NameForAtom(target),
|
|
NameForAtom(property), NameForAtom(prop->type));
|
|
|
|
if (target != property)
|
|
return;
|
|
|
|
if (target == xaTARGETS) {
|
|
if (prop->format != 32)
|
|
return;
|
|
if (prop->type != XA_ATOM)
|
|
return;
|
|
|
|
if (probing) {
|
|
if (vncHasAtom(xaSTRING, (const Atom*)prop->data, prop->size) ||
|
|
vncHasAtom(xaUTF8_STRING, (const Atom*)prop->data, prop->size)) {
|
|
LOG_DEBUG("Compatible format found, notifying clients");
|
|
activeSelection = selection;
|
|
vncAnnounceClipboard(TRUE);
|
|
}
|
|
} else {
|
|
if (vncHasAtom(xaUTF8_STRING, (const Atom*)prop->data, prop->size))
|
|
vncSelectionRequest(selection, xaUTF8_STRING);
|
|
else if (vncHasAtom(xaSTRING, (const Atom*)prop->data, prop->size))
|
|
vncSelectionRequest(selection, xaSTRING);
|
|
}
|
|
} else if (target == xaSTRING) {
|
|
char* filtered;
|
|
char* utf8;
|
|
|
|
if (prop->format != 8)
|
|
return;
|
|
if (prop->type != xaSTRING)
|
|
return;
|
|
|
|
filtered = vncConvertLF(prop->data, prop->size);
|
|
if (filtered == NULL)
|
|
return;
|
|
|
|
utf8 = vncLatin1ToUTF8(filtered, (size_t)-1);
|
|
vncStrFree(filtered);
|
|
if (utf8 == NULL)
|
|
return;
|
|
|
|
LOG_DEBUG("Sending clipboard to clients (%d bytes)",
|
|
(int)strlen(utf8));
|
|
|
|
vncSendClipboardData(utf8);
|
|
|
|
vncStrFree(utf8);
|
|
} else if (target == xaUTF8_STRING) {
|
|
char *filtered;
|
|
|
|
if (prop->format != 8)
|
|
return;
|
|
if (prop->type != xaUTF8_STRING)
|
|
return;
|
|
|
|
filtered = vncConvertLF(prop->data, prop->size);
|
|
if (filtered == NULL)
|
|
return;
|
|
|
|
LOG_DEBUG("Sending clipboard to clients (%d bytes)",
|
|
(int)strlen(filtered));
|
|
|
|
vncSendClipboardData(filtered);
|
|
|
|
vncStrFree(filtered);
|
|
}
|
|
}
|
|
|
|
#define SEND_EVENT_BIT 0x80
|
|
|
|
static int vncProcSendEvent(ClientPtr client)
|
|
{
|
|
REQUEST(xSendEventReq);
|
|
REQUEST_SIZE_MATCH(xSendEventReq);
|
|
|
|
stuff->event.u.u.type &= ~(SEND_EVENT_BIT);
|
|
|
|
if (stuff->event.u.u.type == SelectionNotify &&
|
|
stuff->event.u.selectionNotify.requestor == wid) {
|
|
TimeStamp time;
|
|
time = ClientTimeToServerTime(stuff->event.u.selectionNotify.time);
|
|
vncHandleSelection(stuff->event.u.selectionNotify.selection,
|
|
stuff->event.u.selectionNotify.target,
|
|
stuff->event.u.selectionNotify.property,
|
|
stuff->event.u.selectionNotify.requestor,
|
|
time);
|
|
}
|
|
|
|
return origProcSendEvent(client);
|
|
}
|
|
|
|
static void vncSelectionCallback(CallbackListPtr *callbacks,
|
|
void * data, void * args)
|
|
{
|
|
SelectionInfoRec *info = (SelectionInfoRec *) args;
|
|
|
|
if (info->selection->selection == activeSelection) {
|
|
LOG_DEBUG("Local clipboard lost, notifying clients");
|
|
activeSelection = None;
|
|
vncAnnounceClipboard(FALSE);
|
|
}
|
|
|
|
if (info->kind != SelectionSetOwner)
|
|
return;
|
|
if (info->client == serverClient)
|
|
return;
|
|
|
|
LOG_DEBUG("Selection owner change for %s",
|
|
NameForAtom(info->selection->selection));
|
|
|
|
/*
|
|
* If we're the previous owner of this selection, then we're also the
|
|
* owner of _the other_ selection. Make sure we drop all ownerships so
|
|
* we either own both selections or nonw.
|
|
*/
|
|
DeleteWindowFromAnySelections(pWindow);
|
|
|
|
if ((info->selection->selection != xaPRIMARY) &&
|
|
(info->selection->selection != xaCLIPBOARD))
|
|
return;
|
|
|
|
if ((info->selection->selection == xaPRIMARY) &&
|
|
!vncGetSendPrimary())
|
|
return;
|
|
|
|
LOG_DEBUG("Got clipboard notification, probing for formats");
|
|
|
|
probing = TRUE;
|
|
vncSelectionRequest(info->selection->selection, xaTARGETS);
|
|
}
|
|
|
|
static void vncClientStateCallback(CallbackListPtr * l,
|
|
void * d, void * p)
|
|
{
|
|
ClientPtr client = ((NewClientInfoRec*)p)->client;
|
|
if (client->clientState == ClientStateGone) {
|
|
struct VncDataTarget** nextPtr = &vncDataTargetHead;
|
|
for (struct VncDataTarget* cur = vncDataTargetHead; cur; cur = *nextPtr) {
|
|
if (cur->client == client) {
|
|
*nextPtr = cur->next;
|
|
free(cur);
|
|
continue;
|
|
}
|
|
nextPtr = &cur->next;
|
|
}
|
|
}
|
|
}
|