mirror of
https://github.com/unrealircd/unrealircd.git
synced 2026-07-03 00:03:14 +02:00
23116d344a
aChannel to Channel, and some more. Third party module coders will love this. But.. it makes things more logical and the doxygen output will look more clean and logical as well. (More changes will follow)
665 lines
16 KiB
C
665 lines
16 KiB
C
/*
|
|
* websocket - WebSocket support (RFC6455)
|
|
* (C)Copyright 2016 Bram Matthys and the UnrealIRCd team
|
|
* License: GPLv2
|
|
* This module was sponsored by Aberrant Software Inc.
|
|
*/
|
|
|
|
#include "unrealircd.h"
|
|
#include <limits.h>
|
|
|
|
#define WEBSOCKET_VERSION "1.0.0"
|
|
|
|
ModuleHeader MOD_HEADER(websocket)
|
|
= {
|
|
"websocket",
|
|
WEBSOCKET_VERSION,
|
|
"WebSocket support (RFC6455)",
|
|
"UnrealIRCd Team",
|
|
"unrealircd-5",
|
|
};
|
|
|
|
#if CHAR_MIN < 0
|
|
#error "In UnrealIRCd char should always be unsigned. Check your compiler"
|
|
#endif
|
|
|
|
typedef struct WebSocketUser WebSocketUser;
|
|
struct WebSocketUser {
|
|
char get; /**< GET initiated */
|
|
char handshake_completed; /**< Handshake completed, use data frames */
|
|
char *handshake_key; /**< Handshake key (used during handshake) */
|
|
char *lefttoparse; /**< Leftover buffer to parse */
|
|
int lefttoparselen; /**< Length of lefttoparse buffer */
|
|
};
|
|
|
|
#define WSU(cptr) ((WebSocketUser *)moddata_client(cptr, websocket_md).ptr)
|
|
|
|
#define WEBSOCKET_MAGIC_KEY "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" /* see RFC6455 */
|
|
|
|
#define WSOP_CONTINUATION 0x00
|
|
#define WSOP_TEXT 0x01
|
|
#define WSOP_BINARY 0x02
|
|
#define WSOP_CLOSE 0x08
|
|
#define WSOP_PING 0x09
|
|
#define WSOP_PONG 0x0a
|
|
|
|
/* Forward declarations */
|
|
int websocket_packet_out(Client *from, Client *to, Client *intended_to, char **msg, int *length);
|
|
int websocket_packet_in(Client *sptr, char *readbuf, int *length);
|
|
void websocket_mdata_free(ModData *m);
|
|
int websocket_handle_packet(Client *sptr, char *readbuf, int length);
|
|
int websocket_handle_handshake(Client *sptr, char *readbuf, int *length);
|
|
int websocket_complete_handshake(Client *sptr);
|
|
int websocket_handle_packet_ping(Client *sptr, char *buf, int len);
|
|
int websocket_handle_packet_pong(Client *sptr, char *buf, int len);
|
|
int websocket_create_frame(int opcode, char **buf, int *len);
|
|
int websocket_send_frame(Client *sptr, int opcode, char *buf, int len);
|
|
|
|
/* Global variables */
|
|
ModDataInfo *websocket_md;
|
|
|
|
MOD_INIT(websocket)
|
|
{
|
|
ModDataInfo mreq;
|
|
|
|
MARK_AS_OFFICIAL_MODULE(modinfo);
|
|
|
|
HookAdd(modinfo->handle, HOOKTYPE_PACKET, 0, websocket_packet_out);
|
|
HookAdd(modinfo->handle, HOOKTYPE_RAWPACKET_IN, 0, websocket_packet_in);
|
|
|
|
memset(&mreq, 0, sizeof(mreq));
|
|
mreq.name = "websocket";
|
|
mreq.serialize = NULL;
|
|
mreq.unserialize = NULL;
|
|
mreq.free = websocket_mdata_free;
|
|
mreq.sync = 0;
|
|
mreq.type = MODDATATYPE_CLIENT;
|
|
websocket_md = ModDataAdd(modinfo->handle, mreq);
|
|
|
|
return MOD_SUCCESS;
|
|
}
|
|
|
|
MOD_LOAD(websocket)
|
|
{
|
|
if (SHOWCONNECTINFO)
|
|
{
|
|
config_warn("I'm disabling set::options::show-connect-info for you "
|
|
"as this setting is incompatible with the websocket module.");
|
|
SHOWCONNECTINFO = 0;
|
|
}
|
|
|
|
return MOD_SUCCESS;
|
|
}
|
|
|
|
MOD_UNLOAD(websocket)
|
|
{
|
|
return MOD_SUCCESS;
|
|
}
|
|
|
|
/** UnrealIRCd internals: free WebSocketUser object. */
|
|
void websocket_mdata_free(ModData *m)
|
|
{
|
|
WebSocketUser *wsu = (WebSocketUser *)m->ptr;
|
|
if (wsu)
|
|
{
|
|
safefree(wsu->handshake_key);
|
|
safefree(wsu->lefttoparse);
|
|
MyFree(m->ptr);
|
|
}
|
|
}
|
|
|
|
/** Outgoing packet hook.
|
|
* This transforms the output to be Websocket-compliant, if necessary.
|
|
*/
|
|
int websocket_packet_out(Client *from, Client *to, Client *intended_to, char **msg, int *length)
|
|
{
|
|
if (MyConnect(to) && WSU(to) && WSU(to)->handshake_completed)
|
|
{
|
|
websocket_create_frame(WSOP_BINARY, msg, length);
|
|
return 0;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int websocket_handle_websocket(Client *sptr, char *readbuf2, int length2)
|
|
{
|
|
int n;
|
|
char *ptr;
|
|
int length;
|
|
int length1 = WSU(sptr)->lefttoparselen;
|
|
char readbuf[4096];
|
|
|
|
length = length1 + length2;
|
|
if (length > sizeof(readbuf)-1)
|
|
{
|
|
dead_link(sptr, "Illegal buffer stacking/Excess flood");
|
|
return 0;
|
|
}
|
|
|
|
if (length1 > 0)
|
|
memcpy(readbuf, WSU(sptr)->lefttoparse, length1);
|
|
memcpy(readbuf+length1, readbuf2, length2);
|
|
|
|
safefree(WSU(sptr)->lefttoparse);
|
|
WSU(sptr)->lefttoparselen = 0;
|
|
|
|
ptr = readbuf;
|
|
do {
|
|
n = websocket_handle_packet(sptr, ptr, length);
|
|
if (n < 0)
|
|
return -1; /* killed -- STOP processing */
|
|
if (n == 0)
|
|
{
|
|
/* Short read. Stop processing for now, but save data for next time */
|
|
safefree(WSU(sptr)->lefttoparse);
|
|
WSU(sptr)->lefttoparse = MyMallocEx(length);
|
|
WSU(sptr)->lefttoparselen = length;
|
|
memcpy(WSU(sptr)->lefttoparse, ptr, length);
|
|
return 0;
|
|
}
|
|
length -= n;
|
|
ptr += n;
|
|
if (length < 0)
|
|
abort(); /* less than 0 is impossible */
|
|
} while(length > 0);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/** Incoming packet hook.
|
|
* This processes Websocket frames, if this is a websocket connection.
|
|
* NOTE The different return values:
|
|
* -1 means: don't touch this client anymore, it has or might have been killed!
|
|
* 0 means: don't process this data, but you can read another packet if you want
|
|
* >0 means: process this data (regular IRC data, non-websocket stuff)
|
|
*/
|
|
int websocket_packet_in(Client *sptr, char *readbuf, int *length)
|
|
{
|
|
if ((sptr->local->receiveM == 0) && !WSU(sptr) && (*length > 8) && !strncmp(readbuf, "GET ", 4))
|
|
{
|
|
/* Allocate a new WebSocketUser struct for this session */
|
|
moddata_client(sptr, websocket_md).ptr = MyMallocEx(sizeof(WebSocketUser));
|
|
WSU(sptr)->get = 1;
|
|
}
|
|
|
|
if (!WSU(sptr))
|
|
return 1; /* "normal" IRC client */
|
|
|
|
if (WSU(sptr)->handshake_completed)
|
|
return websocket_handle_websocket(sptr, readbuf, *length);
|
|
/* else.. */
|
|
return websocket_handle_handshake(sptr, readbuf, length);
|
|
}
|
|
|
|
/** Helper function to parse the HTTP header consisting of multiple 'Key: value' pairs */
|
|
int websocket_handshake_helper(char *buffer, int len, char **key, char **value, char **lastloc, int *end_of_request)
|
|
{
|
|
static char buf[4096], *nextptr;
|
|
char *p;
|
|
char *k = NULL, *v = NULL;
|
|
int foundlf = 0;
|
|
|
|
if (buffer)
|
|
{
|
|
/* Initialize */
|
|
if (len > sizeof(buf) - 1)
|
|
len = sizeof(buf) - 1;
|
|
|
|
memcpy(buf, buffer, len);
|
|
buf[len] = '\0';
|
|
nextptr = buf;
|
|
}
|
|
|
|
*end_of_request = 0;
|
|
|
|
p = nextptr;
|
|
|
|
if (!p)
|
|
{
|
|
*key = *value = NULL;
|
|
return 0; /* done processing data */
|
|
}
|
|
|
|
if (!strncmp(p, "\n", 1) || !strncmp(p, "\r\n", 2))
|
|
{
|
|
*key = *value = NULL;
|
|
*end_of_request = 1;
|
|
return 0;
|
|
}
|
|
|
|
/* Note: p *could* point to the NUL byte ('\0') */
|
|
|
|
/* Special handling for GET line itself. */
|
|
if (!strncmp(p, "GET ", 4))
|
|
{
|
|
k = "GET";
|
|
p += 4;
|
|
v = p; /* SET VALUE */
|
|
nextptr = NULL; /* set to "we are done" in case next for loop fails */
|
|
for (; *p; p++)
|
|
{
|
|
if (*p == ' ')
|
|
{
|
|
*p = '\0'; /* terminate before "HTTP/1.X" part */
|
|
}
|
|
else if (*p == '\r')
|
|
{
|
|
*p = '\0'; /* eat silently, but don't consider EOL */
|
|
}
|
|
else if (*p == '\n')
|
|
{
|
|
*p = '\0';
|
|
nextptr = p+1; /* safe, there is data or at least a \0 there */
|
|
break;
|
|
}
|
|
}
|
|
*key = k;
|
|
*value = v;
|
|
return 1;
|
|
}
|
|
|
|
/* Header parsing starts here.
|
|
* Example line "Host: www.unrealircd.org"
|
|
*/
|
|
k = p; /* SET KEY */
|
|
|
|
/* First check if the line contains a terminating \n. If not, don't process it
|
|
* as it may have been a cut header.
|
|
*/
|
|
for (; *p; p++)
|
|
{
|
|
if (*p == '\n')
|
|
{
|
|
foundlf = 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!foundlf)
|
|
{
|
|
*key = *value = NULL;
|
|
*lastloc = k;
|
|
return 0;
|
|
}
|
|
|
|
p = k;
|
|
|
|
for (; *p; p++)
|
|
{
|
|
if ((*p == '\n') || (*p == '\r'))
|
|
{
|
|
/* Reached EOL but 'value' not found */
|
|
*p = '\0';
|
|
break;
|
|
}
|
|
if (*p == ':')
|
|
{
|
|
*p++ = '\0';
|
|
if (*p++ != ' ')
|
|
break; /* missing mandatory space after ':' */
|
|
|
|
v = p; /* SET VALUE */
|
|
nextptr = NULL; /* set to "we are done" in case next for loop fails */
|
|
for (; *p; p++)
|
|
{
|
|
if (*p == '\r')
|
|
{
|
|
*p = '\0'; /* eat silently, but don't consider EOL */
|
|
}
|
|
else if (*p == '\n')
|
|
{
|
|
*p = '\0';
|
|
nextptr = p+1; /* safe, there is data or at least a \0 there */
|
|
break;
|
|
}
|
|
}
|
|
/* A key-value pair was succesfully parsed, return it */
|
|
*key = k;
|
|
*value = v;
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
/* Fatal parse error */
|
|
*key = *value = NULL;
|
|
return 0;
|
|
}
|
|
|
|
/** Handle client GET WebSocket handshake.
|
|
* Yes, I'm going to assume that the header fits in one packet and one packet only.
|
|
*/
|
|
int websocket_handle_handshake(Client *sptr, char *readbuf, int *length)
|
|
{
|
|
char *key, *value;
|
|
int r, end_of_request;
|
|
char netbuf[2048];
|
|
char *lastloc = NULL;
|
|
int n, maxcopy, nprefix=0;
|
|
|
|
/** Frame re-assembling starts here **/
|
|
*netbuf = '\0';
|
|
if (WSU(sptr)->lefttoparse)
|
|
{
|
|
strlcpy(netbuf, WSU(sptr)->lefttoparse, sizeof(netbuf));
|
|
nprefix = strlen(netbuf);
|
|
}
|
|
maxcopy = sizeof(netbuf) - nprefix - 1;
|
|
/* (Need to some manual checking here as strlen() can't be safely used
|
|
* on readbuf. Same is true for strlncat since it uses strlen().)
|
|
*/
|
|
n = *length;
|
|
if (n > maxcopy)
|
|
n = maxcopy;
|
|
if (n <= 0)
|
|
{
|
|
dead_link(sptr, "Oversized line");
|
|
return -1;
|
|
}
|
|
memcpy(netbuf+nprefix, readbuf, n); /* SAFE: see checking above */
|
|
netbuf[n+nprefix] = '\0';
|
|
safefree(WSU(sptr)->lefttoparse);
|
|
|
|
/** Now step through the lines.. **/
|
|
for (r = websocket_handshake_helper(netbuf, strlen(netbuf), &key, &value, &lastloc, &end_of_request);
|
|
r;
|
|
r = websocket_handshake_helper(NULL, 0, &key, &value, &lastloc, &end_of_request))
|
|
{
|
|
if (!strcasecmp(key, "Sec-WebSocket-Key"))
|
|
{
|
|
if (strchr(value, ':'))
|
|
{
|
|
/* This would cause unserialization issues. Should be base64 anyway */
|
|
dead_link(sptr, "Invalid characters in Sec-WebSocket-Key");
|
|
return -1;
|
|
}
|
|
safestrdup(WSU(sptr)->handshake_key, value);
|
|
}
|
|
}
|
|
|
|
if (end_of_request)
|
|
{
|
|
if (!WSU(sptr)->handshake_key)
|
|
{
|
|
if (is_module_loaded("webredir"))
|
|
{
|
|
char *parx[2] = { NULL, NULL };
|
|
do_cmd(sptr, sptr, NULL, "GET", 1, parx);
|
|
}
|
|
dead_link(sptr, "Invalid WebSocket request");
|
|
return -1;
|
|
}
|
|
websocket_complete_handshake(sptr);
|
|
return 0;
|
|
}
|
|
|
|
if (lastloc)
|
|
{
|
|
/* Last line was cut somewhere, save it for next round. */
|
|
safefree(WSU(sptr)->lefttoparse);
|
|
WSU(sptr)->lefttoparse = strdup(lastloc);
|
|
}
|
|
return 0; /* don't let UnrealIRCd process this */
|
|
}
|
|
|
|
/** Complete the handshake by sending the appropriate HTTP 101 response etc. */
|
|
int websocket_complete_handshake(Client *sptr)
|
|
{
|
|
char buf[512], hashbuf[64];
|
|
SHA_CTX hash;
|
|
char sha1out[20]; /* 160 bits */
|
|
|
|
WSU(sptr)->handshake_completed = 1;
|
|
|
|
snprintf(buf, sizeof(buf), "%s%s", WSU(sptr)->handshake_key, WEBSOCKET_MAGIC_KEY);
|
|
SHA1_Init(&hash);
|
|
SHA1_Update(&hash, buf, strlen(buf));
|
|
SHA1_Final(sha1out, &hash);
|
|
|
|
b64_encode(sha1out, sizeof(sha1out), hashbuf, sizeof(hashbuf));
|
|
|
|
snprintf(buf, sizeof(buf),
|
|
"HTTP/1.1 101 Switching Protocols\r\n"
|
|
"Upgrade: websocket\r\n"
|
|
"Connection: Upgrade\r\n"
|
|
"Sec-WebSocket-Accept: %s\r\n"
|
|
"\r\n",
|
|
hashbuf);
|
|
|
|
/* Caution: we bypass sendQ flood checking by doing it this way.
|
|
* Risk is minimal, though, as we only permit limited text only
|
|
* once per session.
|
|
*/
|
|
dbuf_put(&sptr->local->sendQ, buf, strlen(buf));
|
|
send_queued(sptr);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Add LF (if needed) to a buffer. Max 4K. */
|
|
void add_lf_if_needed(char **buf, int *len)
|
|
{
|
|
static char newbuf[4096];
|
|
char *b = *buf;
|
|
int l = *len;
|
|
|
|
if (l <= 0)
|
|
return; /* too short */
|
|
|
|
if (b[l - 1] == '\n')
|
|
return; /* already contains \n */
|
|
|
|
if (l >= sizeof(newbuf)-2)
|
|
l = sizeof(newbuf)-2; /* cut-off if necessary */
|
|
|
|
memcpy(newbuf, b, l);
|
|
newbuf[l] = '\n';
|
|
newbuf[l + 1] = '\0'; /* not necessary, but I like zero termination */
|
|
l++;
|
|
*buf = newbuf; /* new buffer */
|
|
*len = l; /* new length */
|
|
}
|
|
|
|
/** WebSocket packet handler.
|
|
* For more information on the format, check out page 28 of RFC6455.
|
|
* @returns The number of bytes processed (the size of the frame)
|
|
* OR 0 to indicate a possible short read (want more data)
|
|
* OR -1 in case of an error.
|
|
*/
|
|
int websocket_handle_packet(Client *sptr, char *readbuf, int length)
|
|
{
|
|
char fin; /**< Final fragment */
|
|
char opcode; /**< Opcode */
|
|
char masked; /**< Masked */
|
|
int len; /**< Length of the packet */
|
|
char maskkey[4]; /**< Key used for masking */
|
|
char *p, *payload;
|
|
int total_packet_size;
|
|
|
|
if (length < 4)
|
|
{
|
|
/* WebSocket packet too short */
|
|
return 0;
|
|
}
|
|
|
|
fin = readbuf[0] & 0x80;
|
|
opcode = readbuf[0] & 0x7F;
|
|
masked = readbuf[1] & 0x80;
|
|
len = readbuf[1] & 0x7F;
|
|
p = &readbuf[2]; /* point to next element */
|
|
|
|
/* actually 'fin' is unused.. we don't care. */
|
|
|
|
if (!masked)
|
|
{
|
|
dead_link(sptr, "WebSocket packet not masked");
|
|
return -1; /* Having the masked bit set is required (RFC6455 p29) */
|
|
}
|
|
|
|
if (len == 127)
|
|
{
|
|
dead_link(sptr, "WebSocket packet with insane size");
|
|
return -1; /* Packets requiring 64bit lengths are not supported. Would be insane. */
|
|
}
|
|
|
|
total_packet_size = len + 2 + 4; /* 2 for header, 4 for mask key, rest for payload */
|
|
|
|
/* Early (minimal) length check */
|
|
if (length < total_packet_size)
|
|
{
|
|
/* WebSocket frame too short */
|
|
return 0;
|
|
}
|
|
|
|
/* Len=126 is special. It indicates the data length is actually "126 or more" */
|
|
if (len == 126)
|
|
{
|
|
/* Extended payload length (16 bit). For packets of >=126 bytes */
|
|
len = (readbuf[2] << 8) + readbuf[3];
|
|
if (len < 126)
|
|
{
|
|
dead_link(sptr, "WebSocket protocol violation (extended payload length too short)");
|
|
return -1; /* This is a violation (not a short read), see page 29 */
|
|
}
|
|
p += 2; /* advance pointer 2 bytes */
|
|
|
|
/* Need to check the length again, now it has changed: */
|
|
if (length < len + 4 + 4)
|
|
{
|
|
/* WebSocket frame too short */
|
|
return 0;
|
|
}
|
|
/* And update the packet size */
|
|
total_packet_size = len + 4 + 4; /* 4 for header, 4 for mask key, rest for payload */
|
|
}
|
|
|
|
memcpy(maskkey, p, 4);
|
|
p+= 4;
|
|
payload = (len > 0) ? p : NULL;
|
|
|
|
if (len > 0)
|
|
{
|
|
/* Unmask this thing (page 33, section 5.3) */
|
|
int n;
|
|
char v;
|
|
for (n = 0; n < len; n++)
|
|
{
|
|
v = *p;
|
|
*p++ = v ^ maskkey[n % 4];
|
|
}
|
|
}
|
|
|
|
switch(opcode)
|
|
{
|
|
case WSOP_CONTINUATION:
|
|
case WSOP_TEXT:
|
|
case WSOP_BINARY:
|
|
if (len > 0)
|
|
{
|
|
add_lf_if_needed(&payload, &len);
|
|
if (!process_packet(sptr, payload, len, 1)) /* let UnrealIRCd process this data */
|
|
return -1; /* fatal error occured (such as flood kill) */
|
|
}
|
|
return total_packet_size;
|
|
|
|
case WSOP_CLOSE:
|
|
dead_link(sptr, "Connection closed"); /* TODO: Improve I guess */
|
|
return -1;
|
|
|
|
case WSOP_PING:
|
|
if (websocket_handle_packet_ping(sptr, payload, len) < 0)
|
|
return -1;
|
|
return total_packet_size;
|
|
|
|
case WSOP_PONG:
|
|
if (websocket_handle_packet_pong(sptr, payload, len) < 0)
|
|
return -1;
|
|
return total_packet_size;
|
|
|
|
default:
|
|
dead_link(sptr, "WebSocket: Unknown opcode");
|
|
return -1;
|
|
}
|
|
|
|
return -1; /* NOTREACHED */
|
|
}
|
|
|
|
int websocket_handle_packet_ping(Client *sptr, char *buf, int len)
|
|
{
|
|
if (len > 500)
|
|
{
|
|
dead_link(sptr, "WebSocket: oversized PING request");
|
|
return -1;
|
|
}
|
|
websocket_send_frame(sptr, WSOP_PONG, buf, len);
|
|
sptr->local->since++; /* lag penalty of 1 second */
|
|
return 0;
|
|
}
|
|
|
|
int websocket_handle_packet_pong(Client *sptr, char *buf, int len)
|
|
{
|
|
/* We don't care */
|
|
return 0;
|
|
}
|
|
|
|
/** Create a frame. Used for OUTGOING data. */
|
|
int websocket_create_frame(int opcode, char **buf, int *len)
|
|
{
|
|
static char sendbuf[8192];
|
|
|
|
sendbuf[0] = opcode | 0x80; /* opcode & final */
|
|
|
|
if (*len > sizeof(sendbuf) - 8)
|
|
abort(); /* should never happen (safety) */
|
|
|
|
/* strip LF */
|
|
if (*len > 0)
|
|
{
|
|
if (*(*buf + *len - 1) == '\n')
|
|
*len = *len - 1;
|
|
}
|
|
/* strip CR */
|
|
if (*len > 0)
|
|
{
|
|
if (*(*buf + *len - 1) == '\r')
|
|
*len = *len - 1;
|
|
}
|
|
|
|
if (*len < 126)
|
|
{
|
|
/* Short payload */
|
|
sendbuf[1] = (char)*len;
|
|
memcpy(&sendbuf[2], *buf, *len);
|
|
*buf = sendbuf;
|
|
*len += 2;
|
|
} else {
|
|
/* Long payload */
|
|
sendbuf[1] = 126;
|
|
sendbuf[2] = (char)((*len >> 8) & 0xFF);
|
|
sendbuf[3] = (char)(*len & 0xFF);
|
|
memcpy(&sendbuf[4], *buf, *len);
|
|
*buf = sendbuf;
|
|
*len += 4;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/** Create and send a frame */
|
|
int websocket_send_frame(Client *sptr, int opcode, char *buf, int len)
|
|
{
|
|
char *b = buf;
|
|
int l = len;
|
|
|
|
if (websocket_create_frame(opcode, &b, &l) < 0)
|
|
return -1;
|
|
|
|
if (DBufLength(&sptr->local->sendQ) > get_sendq(sptr))
|
|
{
|
|
dead_link(sptr, "Max SendQ exceeded");
|
|
return -1;
|
|
}
|
|
|
|
dbuf_put(&sptr->local->sendQ, b, l);
|
|
send_queued(sptr);
|
|
return 0;
|
|
}
|