mirror of
https://git.hardenedbsd.org/hardenedbsd/HardenedBSD.git
synced 2024-11-23 14:41:08 +01:00
a2f733abcf
Apply the following automated changes to try to eliminate no-longer-needed sys/cdefs.h includes as well as now-empty blank lines in a row. Remove /^#if.*\n#endif.*\n#include\s+<sys/cdefs.h>.*\n/ Remove /\n+#include\s+<sys/cdefs.h>.*\n+#if.*\n#endif.*\n+/ Remove /\n+#if.*\n#endif.*\n+/ Remove /^#if.*\n#endif.*\n/ Remove /\n+#include\s+<sys/cdefs.h>\n#include\s+<sys/types.h>/ Remove /\n+#include\s+<sys/cdefs.h>\n#include\s+<sys/param.h>/ Remove /\n+#include\s+<sys/cdefs.h>\n#include\s+<sys/capsicum.h>/ Sponsored by: Netflix
1337 lines
31 KiB
C
1337 lines
31 KiB
C
/*-
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*
|
|
* Copyright (c) 1998, 2001, 2002, Juniper Networks, Inc.
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
|
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
|
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
|
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
* SUCH DAMAGE.
|
|
*/
|
|
|
|
#include <sys/types.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/time.h>
|
|
#include <netinet/in.h>
|
|
#include <arpa/inet.h>
|
|
|
|
#include <assert.h>
|
|
#include <ctype.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <md5.h>
|
|
#include <netdb.h>
|
|
#include <stdarg.h>
|
|
#include <stddef.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
#include <security/pam_appl.h>
|
|
#include <security/openpam.h>
|
|
|
|
#include "taclib_private.h"
|
|
|
|
static int add_str_8(struct tac_handle *, u_int8_t *,
|
|
struct tac_str *);
|
|
static int add_str_16(struct tac_handle *, u_int16_t *,
|
|
struct tac_str *);
|
|
static int protocol_version(int, int, int);
|
|
static void close_connection(struct tac_handle *);
|
|
static int conn_server(struct tac_handle *);
|
|
static void crypt_msg(struct tac_handle *, struct tac_msg *);
|
|
static void *dup_str(struct tac_handle *, const struct tac_str *,
|
|
size_t *);
|
|
static int establish_connection(struct tac_handle *);
|
|
static void free_str(struct tac_str *);
|
|
static void generr(struct tac_handle *, const char *, ...)
|
|
__printflike(2, 3);
|
|
static void gen_session_id(struct tac_msg *);
|
|
static int get_srvr_end(struct tac_handle *);
|
|
static int get_str(struct tac_handle *, const char *,
|
|
struct tac_str *, size_t);
|
|
static void init_str(struct tac_str *);
|
|
static int read_timed(struct tac_handle *, void *, size_t,
|
|
const struct timeval *);
|
|
static int recv_msg(struct tac_handle *);
|
|
static int save_str(struct tac_handle *, struct tac_str *,
|
|
const void *, size_t);
|
|
static int send_msg(struct tac_handle *);
|
|
static int split(char *, char *[], int, char *, size_t);
|
|
static void *xmalloc(struct tac_handle *, size_t);
|
|
static char *xstrdup(struct tac_handle *, const char *);
|
|
static void clear_srvr_avs(struct tac_handle *);
|
|
static void create_msg(struct tac_handle *, int, int, int);
|
|
|
|
/*
|
|
* Append some optional data to the current request, and store its
|
|
* length into the 8-bit field referenced by "fld". Returns 0 on
|
|
* success, or -1 on failure.
|
|
*
|
|
* This function also frees the "cs" string data and initializes it
|
|
* for the next time.
|
|
*/
|
|
static int
|
|
add_str_8(struct tac_handle *h, u_int8_t *fld, struct tac_str *cs)
|
|
{
|
|
u_int16_t len;
|
|
|
|
if (add_str_16(h, &len, cs) == -1)
|
|
return -1;
|
|
len = ntohs(len);
|
|
if (len > 0xff) {
|
|
generr(h, "Field too long");
|
|
return -1;
|
|
}
|
|
*fld = len;
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Append some optional data to the current request, and store its
|
|
* length into the 16-bit field (network byte order) referenced by
|
|
* "fld". Returns 0 on success, or -1 on failure.
|
|
*
|
|
* This function also frees the "cs" string data and initializes it
|
|
* for the next time.
|
|
*/
|
|
static int
|
|
add_str_16(struct tac_handle *h, u_int16_t *fld, struct tac_str *cs)
|
|
{
|
|
size_t len;
|
|
|
|
len = cs->len;
|
|
if (cs->data == NULL)
|
|
len = 0;
|
|
if (len != 0) {
|
|
int offset;
|
|
|
|
if (len > 0xffff) {
|
|
generr(h, "Field too long");
|
|
return -1;
|
|
}
|
|
offset = ntohl(h->request.length);
|
|
if (offset + len > BODYSIZE) {
|
|
generr(h, "Message too long");
|
|
return -1;
|
|
}
|
|
memcpy(h->request.u.body + offset, cs->data, len);
|
|
h->request.length = htonl(offset + len);
|
|
}
|
|
*fld = htons(len);
|
|
free_str(cs);
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
protocol_version(int msg_type, int var, int type)
|
|
{
|
|
int minor;
|
|
|
|
switch (msg_type) {
|
|
case TAC_AUTHEN:
|
|
/* 'var' represents the 'action' */
|
|
switch (var) {
|
|
case TAC_AUTHEN_LOGIN:
|
|
switch (type) {
|
|
|
|
case TAC_AUTHEN_TYPE_PAP:
|
|
case TAC_AUTHEN_TYPE_CHAP:
|
|
case TAC_AUTHEN_TYPE_MSCHAP:
|
|
case TAC_AUTHEN_TYPE_ARAP:
|
|
minor = 1;
|
|
break;
|
|
|
|
default:
|
|
minor = 0;
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case TAC_AUTHEN_SENDAUTH:
|
|
minor = 1;
|
|
break;
|
|
|
|
default:
|
|
minor = 0;
|
|
break;
|
|
};
|
|
break;
|
|
|
|
case TAC_AUTHOR:
|
|
/* 'var' represents the 'method' */
|
|
switch (var) {
|
|
/*
|
|
* When new authentication methods are added, include 'method'
|
|
* in determining the value of 'minor'. At this point, all
|
|
* methods defined in this implementation (see "Authorization
|
|
* authentication methods" in taclib.h) are minor version 0
|
|
* Not all types, however, indicate minor version 0.
|
|
*/
|
|
case TAC_AUTHEN_METH_NOT_SET:
|
|
case TAC_AUTHEN_METH_NONE:
|
|
case TAC_AUTHEN_METH_KRB5:
|
|
case TAC_AUTHEN_METH_LINE:
|
|
case TAC_AUTHEN_METH_ENABLE:
|
|
case TAC_AUTHEN_METH_LOCAL:
|
|
case TAC_AUTHEN_METH_TACACSPLUS:
|
|
case TAC_AUTHEN_METH_RCMD:
|
|
switch (type) {
|
|
case TAC_AUTHEN_TYPE_PAP:
|
|
case TAC_AUTHEN_TYPE_CHAP:
|
|
case TAC_AUTHEN_TYPE_MSCHAP:
|
|
case TAC_AUTHEN_TYPE_ARAP:
|
|
minor = 1;
|
|
break;
|
|
|
|
default:
|
|
minor = 0;
|
|
break;
|
|
}
|
|
break;
|
|
default:
|
|
minor = 0;
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case TAC_ACCT:
|
|
|
|
default:
|
|
minor = 0;
|
|
break;
|
|
}
|
|
|
|
return TAC_VER_MAJOR << 4 | minor;
|
|
}
|
|
|
|
|
|
static void
|
|
close_connection(struct tac_handle *h)
|
|
{
|
|
if (h->fd != -1) {
|
|
close(h->fd);
|
|
h->fd = -1;
|
|
}
|
|
}
|
|
|
|
static int
|
|
conn_server(struct tac_handle *h)
|
|
{
|
|
struct tac_server *srvp = &h->servers[h->cur_server];
|
|
int flags;
|
|
|
|
if ((h->fd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1) {
|
|
generr(h, "Cannot create socket: %s", strerror(errno));
|
|
return -1;
|
|
}
|
|
if ((flags = fcntl(h->fd, F_GETFL, 0)) == -1 ||
|
|
fcntl(h->fd, F_SETFL, flags | O_NONBLOCK) == -1) {
|
|
generr(h, "Cannot set non-blocking mode on socket: %s",
|
|
strerror(errno));
|
|
close(h->fd);
|
|
h->fd = -1;
|
|
return -1;
|
|
}
|
|
if (connect(h->fd, (struct sockaddr *)&srvp->addr,
|
|
sizeof srvp->addr) == 0)
|
|
return 0;
|
|
|
|
if (errno == EINPROGRESS) {
|
|
fd_set wfds;
|
|
struct timeval tv;
|
|
int nfds;
|
|
struct sockaddr peer;
|
|
socklen_t errlen, peerlen;
|
|
int err;
|
|
|
|
/* Wait for the connection to complete. */
|
|
FD_ZERO(&wfds);
|
|
FD_SET(h->fd, &wfds);
|
|
tv.tv_sec = srvp->timeout;
|
|
tv.tv_usec = 0;
|
|
nfds = select(h->fd + 1, NULL, &wfds, NULL, &tv);
|
|
if (nfds == -1) {
|
|
generr(h, "select: %s", strerror(errno));
|
|
close(h->fd);
|
|
h->fd = -1;
|
|
return -1;
|
|
}
|
|
if (nfds == 0) {
|
|
generr(h, "connect: timed out");
|
|
close(h->fd);
|
|
h->fd = -1;
|
|
return -1;
|
|
}
|
|
|
|
/* See whether we are connected now. */
|
|
peerlen = sizeof peer;
|
|
if (getpeername(h->fd, &peer, &peerlen) == 0)
|
|
return 0;
|
|
|
|
if (errno != ENOTCONN) {
|
|
generr(h, "getpeername: %s", strerror(errno));
|
|
close(h->fd);
|
|
h->fd = -1;
|
|
return -1;
|
|
}
|
|
|
|
/* Find out why the connect failed. */
|
|
errlen = sizeof err;
|
|
getsockopt(h->fd, SOL_SOCKET, SO_ERROR, &err, &errlen);
|
|
errno = err;
|
|
}
|
|
generr(h, "connect: %s", strerror(errno));
|
|
close(h->fd);
|
|
h->fd = -1;
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
* Encrypt or decrypt a message. The operations are symmetrical.
|
|
*/
|
|
static void
|
|
crypt_msg(struct tac_handle *h, struct tac_msg *msg)
|
|
{
|
|
const char *secret;
|
|
MD5_CTX base_ctx;
|
|
MD5_CTX ctx;
|
|
unsigned char md5[16];
|
|
int chunk;
|
|
int msg_len;
|
|
|
|
secret = h->servers[h->cur_server].secret;
|
|
if (secret[0] == '\0')
|
|
msg->flags |= TAC_UNENCRYPTED;
|
|
if (msg->flags & TAC_UNENCRYPTED)
|
|
return;
|
|
|
|
msg_len = ntohl(msg->length);
|
|
|
|
MD5Init(&base_ctx);
|
|
MD5Update(&base_ctx, msg->session_id, sizeof msg->session_id);
|
|
MD5Update(&base_ctx, secret, strlen(secret));
|
|
MD5Update(&base_ctx, &msg->version, sizeof msg->version);
|
|
MD5Update(&base_ctx, &msg->seq_no, sizeof msg->seq_no);
|
|
|
|
ctx = base_ctx;
|
|
for (chunk = 0; chunk < msg_len; chunk += sizeof md5) {
|
|
int chunk_len;
|
|
int i;
|
|
|
|
MD5Final(md5, &ctx);
|
|
|
|
if ((chunk_len = msg_len - chunk) > sizeof md5)
|
|
chunk_len = sizeof md5;
|
|
for (i = 0; i < chunk_len; i++)
|
|
msg->u.body[chunk + i] ^= md5[i];
|
|
|
|
ctx = base_ctx;
|
|
MD5Update(&ctx, md5, sizeof md5);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Return a dynamically allocated copy of the given server string.
|
|
* The copy is null-terminated. If "len" is non-NULL, the length of
|
|
* the string (excluding the terminating null byte) is stored via it.
|
|
* Returns NULL on failure. Empty strings are still allocated even
|
|
* though they have no content.
|
|
*/
|
|
static void *
|
|
dup_str(struct tac_handle *h, const struct tac_str *ss, size_t *len)
|
|
{
|
|
unsigned char *p;
|
|
|
|
if ((p = (unsigned char *)xmalloc(h, ss->len + 1)) == NULL)
|
|
return NULL;
|
|
if (ss->data != NULL && ss->len != 0)
|
|
memcpy(p, ss->data, ss->len);
|
|
p[ss->len] = '\0';
|
|
if (len != NULL)
|
|
*len = ss->len;
|
|
return p;
|
|
}
|
|
|
|
static int
|
|
establish_connection(struct tac_handle *h)
|
|
{
|
|
int i;
|
|
|
|
if (h->fd >= 0) /* Already connected. */
|
|
return 0;
|
|
if (h->num_servers == 0) {
|
|
generr(h, "No TACACS+ servers specified");
|
|
return -1;
|
|
}
|
|
/*
|
|
* Try the servers round-robin. We begin with the one that
|
|
* worked for us the last time. That way, once we find a good
|
|
* server, we won't waste any more time trying the bad ones.
|
|
*/
|
|
for (i = 0; i < h->num_servers; i++) {
|
|
if (conn_server(h) == 0) {
|
|
h->single_connect = (h->servers[h->cur_server].flags &
|
|
TAC_SRVR_SINGLE_CONNECT) != 0;
|
|
return 0;
|
|
}
|
|
if (++h->cur_server >= h->num_servers) /* Wrap around */
|
|
h->cur_server = 0;
|
|
}
|
|
/* Just return whatever error was last reported by conn_server(). */
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
* Free a client string, obliterating its contents first for security.
|
|
*/
|
|
static void
|
|
free_str(struct tac_str *cs)
|
|
{
|
|
if (cs->data != NULL) {
|
|
memset_s(cs->data, cs->len, 0, cs->len);
|
|
free(cs->data);
|
|
cs->data = NULL;
|
|
cs->len = 0;
|
|
}
|
|
}
|
|
|
|
static void
|
|
generr(struct tac_handle *h, const char *format, ...)
|
|
{
|
|
va_list ap;
|
|
|
|
va_start(ap, format);
|
|
vsnprintf(h->errmsg, ERRSIZE, format, ap);
|
|
va_end(ap);
|
|
}
|
|
|
|
static void
|
|
gen_session_id(struct tac_msg *msg)
|
|
{
|
|
int r;
|
|
|
|
r = arc4random();
|
|
msg->session_id[0] = r >> 8;
|
|
msg->session_id[1] = r;
|
|
r = arc4random();
|
|
msg->session_id[2] = r >> 8;
|
|
msg->session_id[3] = r;
|
|
}
|
|
|
|
/*
|
|
* Verify that we are exactly at the end of the response message.
|
|
* Returns 0 on success, -1 on failure.
|
|
*/
|
|
static int
|
|
get_srvr_end(struct tac_handle *h)
|
|
{
|
|
int len;
|
|
|
|
len = ntohl(h->response.length);
|
|
|
|
if (h->srvr_pos != len) {
|
|
generr(h, "Invalid length field in response "
|
|
"from server: end expected at %u, response length %u",
|
|
h->srvr_pos, len);
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
get_str(struct tac_handle *h, const char *field,
|
|
struct tac_str *ss, size_t len)
|
|
{
|
|
if (h->srvr_pos + len > ntohl(h->response.length)) {
|
|
generr(h, "Invalid length field in %s response from server "
|
|
"(%lu > %lu)", field, (u_long)(h->srvr_pos + len),
|
|
(u_long)ntohl(h->response.length));
|
|
return -1;
|
|
}
|
|
ss->data = len != 0 ? h->response.u.body + h->srvr_pos : NULL;
|
|
ss->len = len;
|
|
h->srvr_pos += len;
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
init_str(struct tac_str *cs)
|
|
{
|
|
cs->data = NULL;
|
|
cs->len = 0;
|
|
}
|
|
|
|
static int
|
|
read_timed(struct tac_handle *h, void *buf, size_t len,
|
|
const struct timeval *deadline)
|
|
{
|
|
char *ptr;
|
|
|
|
ptr = (char *)buf;
|
|
while (len > 0) {
|
|
int n;
|
|
|
|
n = read(h->fd, ptr, len);
|
|
if (n == -1) {
|
|
struct timeval tv;
|
|
int nfds;
|
|
|
|
if (errno != EAGAIN) {
|
|
generr(h, "Network read error: %s",
|
|
strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
/* Wait until we can read more data. */
|
|
gettimeofday(&tv, NULL);
|
|
timersub(deadline, &tv, &tv);
|
|
if (tv.tv_sec >= 0) {
|
|
fd_set rfds;
|
|
|
|
FD_ZERO(&rfds);
|
|
FD_SET(h->fd, &rfds);
|
|
nfds =
|
|
select(h->fd + 1, &rfds, NULL, NULL, &tv);
|
|
if (nfds == -1) {
|
|
generr(h, "select: %s",
|
|
strerror(errno));
|
|
return -1;
|
|
}
|
|
} else
|
|
nfds = 0;
|
|
if (nfds == 0) {
|
|
generr(h, "Network read timed out");
|
|
return -1;
|
|
}
|
|
} else if (n == 0) {
|
|
generr(h, "unexpected EOF from server");
|
|
return -1;
|
|
} else {
|
|
ptr += n;
|
|
len -= n;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Receive a response from the server and decrypt it. Returns 0 on
|
|
* success, or -1 on failure.
|
|
*/
|
|
static int
|
|
recv_msg(struct tac_handle *h)
|
|
{
|
|
struct timeval deadline;
|
|
struct tac_msg *msg;
|
|
u_int32_t len;
|
|
|
|
msg = &h->response;
|
|
gettimeofday(&deadline, NULL);
|
|
deadline.tv_sec += h->servers[h->cur_server].timeout;
|
|
|
|
/* Read the message header and make sure it is reasonable. */
|
|
if (read_timed(h, msg, HDRSIZE, &deadline) == -1)
|
|
return -1;
|
|
if (memcmp(msg->session_id, h->request.session_id,
|
|
sizeof msg->session_id) != 0) {
|
|
generr(h, "Invalid session ID in received message");
|
|
return -1;
|
|
}
|
|
if (msg->type != h->request.type) {
|
|
generr(h, "Invalid type in received message"
|
|
" (got %u, expected %u)",
|
|
msg->type, h->request.type);
|
|
return -1;
|
|
}
|
|
len = ntohl(msg->length);
|
|
if (len > BODYSIZE) {
|
|
generr(h, "Received message too large (%u > %u)",
|
|
len, BODYSIZE);
|
|
return -1;
|
|
}
|
|
if (msg->seq_no != ++h->last_seq_no) {
|
|
generr(h, "Invalid sequence number in received message"
|
|
" (got %u, expected %u)",
|
|
msg->seq_no, h->last_seq_no);
|
|
return -1;
|
|
}
|
|
|
|
/* Read the message body. */
|
|
if (read_timed(h, msg->u.body, len, &deadline) == -1)
|
|
return -1;
|
|
|
|
/* Decrypt it. */
|
|
crypt_msg(h, msg);
|
|
|
|
/*
|
|
* Turn off single-connection mode if the server isn't amenable
|
|
* to it.
|
|
*/
|
|
if (!(msg->flags & TAC_SINGLE_CONNECT))
|
|
h->single_connect = 0;
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
save_str(struct tac_handle *h, struct tac_str *cs, const void *data,
|
|
size_t len)
|
|
{
|
|
free_str(cs);
|
|
if (data != NULL && len != 0) {
|
|
if ((cs->data = xmalloc(h, len)) == NULL)
|
|
return -1;
|
|
cs->len = len;
|
|
memcpy(cs->data, data, len);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Send the current request, after encrypting it. Returns 0 on success,
|
|
* or -1 on failure.
|
|
*/
|
|
static int
|
|
send_msg(struct tac_handle *h)
|
|
{
|
|
struct timeval deadline;
|
|
struct tac_msg *msg;
|
|
char *ptr;
|
|
int len;
|
|
|
|
if (h->last_seq_no & 1) {
|
|
generr(h, "Attempt to send message out of sequence");
|
|
return -1;
|
|
}
|
|
|
|
if (establish_connection(h) == -1)
|
|
return -1;
|
|
|
|
msg = &h->request;
|
|
msg->seq_no = ++h->last_seq_no;
|
|
if (msg->seq_no == 1)
|
|
gen_session_id(msg);
|
|
crypt_msg(h, msg);
|
|
|
|
if (h->single_connect)
|
|
msg->flags |= TAC_SINGLE_CONNECT;
|
|
else
|
|
msg->flags &= ~TAC_SINGLE_CONNECT;
|
|
gettimeofday(&deadline, NULL);
|
|
deadline.tv_sec += h->servers[h->cur_server].timeout;
|
|
len = HDRSIZE + ntohl(msg->length);
|
|
ptr = (char *)msg;
|
|
while (len > 0) {
|
|
int n;
|
|
|
|
n = write(h->fd, ptr, len);
|
|
if (n == -1) {
|
|
struct timeval tv;
|
|
int nfds;
|
|
|
|
if (errno != EAGAIN) {
|
|
generr(h, "Network write error: %s",
|
|
strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
/* Wait until we can write more data. */
|
|
gettimeofday(&tv, NULL);
|
|
timersub(&deadline, &tv, &tv);
|
|
if (tv.tv_sec >= 0) {
|
|
fd_set wfds;
|
|
|
|
FD_ZERO(&wfds);
|
|
FD_SET(h->fd, &wfds);
|
|
nfds =
|
|
select(h->fd + 1, NULL, &wfds, NULL, &tv);
|
|
if (nfds == -1) {
|
|
generr(h, "select: %s",
|
|
strerror(errno));
|
|
return -1;
|
|
}
|
|
} else
|
|
nfds = 0;
|
|
if (nfds == 0) {
|
|
generr(h, "Network write timed out");
|
|
return -1;
|
|
}
|
|
} else {
|
|
ptr += n;
|
|
len -= n;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
tac_add_server_av(struct tac_handle *h, const char *host, int port,
|
|
const char *secret, int timeout, int flags, const char *const *avs)
|
|
{
|
|
struct tac_server *srvp;
|
|
const char *p;
|
|
size_t len;
|
|
int i;
|
|
|
|
if (h->num_servers >= MAXSERVERS) {
|
|
generr(h, "Too many TACACS+ servers specified");
|
|
return -1;
|
|
}
|
|
srvp = &h->servers[h->num_servers];
|
|
|
|
memset(&srvp->addr, 0, sizeof srvp->addr);
|
|
srvp->addr.sin_len = sizeof srvp->addr;
|
|
srvp->addr.sin_family = AF_INET;
|
|
if (!inet_aton(host, &srvp->addr.sin_addr)) {
|
|
struct hostent *hent;
|
|
|
|
if ((hent = gethostbyname(host)) == NULL) {
|
|
generr(h, "%s: host not found", host);
|
|
return -1;
|
|
}
|
|
memcpy(&srvp->addr.sin_addr, hent->h_addr,
|
|
sizeof srvp->addr.sin_addr);
|
|
}
|
|
srvp->addr.sin_port = htons(port != 0 ? port : TACPLUS_PORT);
|
|
if ((srvp->secret = xstrdup(h, secret)) == NULL)
|
|
return -1;
|
|
srvp->timeout = timeout;
|
|
srvp->flags = flags;
|
|
srvp->navs = 0;
|
|
for (i = 0; avs[i] != NULL; i++) {
|
|
if (i >= MAXAVPAIRS) {
|
|
generr(h, "too many AV pairs");
|
|
goto fail;
|
|
}
|
|
for (p = avs[i], len = 0; is_arg(*p); p++)
|
|
len++;
|
|
if (p == avs[i] || *p != '=') {
|
|
generr(h, "invalid AV pair %d", i);
|
|
goto fail;
|
|
}
|
|
while (*p++ != '\0')
|
|
len++;
|
|
if ((srvp->avs[i].data = xstrdup(h, avs[i])) == NULL)
|
|
goto fail;
|
|
srvp->avs[i].len = len;
|
|
srvp->navs++;
|
|
}
|
|
h->num_servers++;
|
|
return 0;
|
|
fail:
|
|
memset_s(srvp->secret, strlen(srvp->secret), 0, strlen(srvp->secret));
|
|
free(srvp->secret);
|
|
srvp->secret = NULL;
|
|
for (i = 0; i < srvp->navs; i++) {
|
|
free(srvp->avs[i].data);
|
|
srvp->avs[i].data = NULL;
|
|
srvp->avs[i].len = 0;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
int
|
|
tac_add_server(struct tac_handle *h, const char *host, int port,
|
|
const char *secret, int timeout, int flags)
|
|
{
|
|
const char *const *avs = { NULL };
|
|
|
|
return tac_add_server_av(h, host, port, secret, timeout, flags, avs);
|
|
}
|
|
|
|
void
|
|
tac_close(struct tac_handle *h)
|
|
{
|
|
int i, srv;
|
|
|
|
if (h->fd != -1)
|
|
close(h->fd);
|
|
for (srv = 0; srv < h->num_servers; srv++) {
|
|
memset(h->servers[srv].secret, 0,
|
|
strlen(h->servers[srv].secret));
|
|
free(h->servers[srv].secret);
|
|
}
|
|
free_str(&h->user);
|
|
free_str(&h->port);
|
|
free_str(&h->rem_addr);
|
|
free_str(&h->data);
|
|
free_str(&h->user_msg);
|
|
for (i=0; i<MAXAVPAIRS; i++)
|
|
free_str(&(h->avs[i]));
|
|
|
|
/* Clear everything else before freeing memory */
|
|
memset(h, 0, sizeof(struct tac_handle));
|
|
free(h);
|
|
}
|
|
|
|
static void
|
|
freev(char **fields, int nfields)
|
|
{
|
|
if (fields != NULL) {
|
|
while (nfields-- > 0)
|
|
free(fields[nfields]);
|
|
free(fields);
|
|
}
|
|
}
|
|
|
|
int
|
|
tac_config(struct tac_handle *h, const char *path)
|
|
{
|
|
FILE *fp;
|
|
char **fields;
|
|
int linenum, nfields;
|
|
int retval;
|
|
|
|
if (path == NULL)
|
|
path = PATH_TACPLUS_CONF;
|
|
if ((fp = fopen(path, "r")) == NULL) {
|
|
generr(h, "Cannot open \"%s\": %s", path, strerror(errno));
|
|
return -1;
|
|
}
|
|
retval = 0;
|
|
linenum = nfields = 0;
|
|
fields = NULL;
|
|
while ((fields = openpam_readlinev(fp, &linenum, &nfields)) != NULL) {
|
|
char *host, *res;
|
|
char *port_str;
|
|
char *secret;
|
|
char *timeout_str;
|
|
char *end;
|
|
unsigned long timeout;
|
|
int port;
|
|
int options;
|
|
int i;
|
|
|
|
if (nfields == 0) {
|
|
freev(fields, nfields);
|
|
continue;
|
|
}
|
|
if (nfields < 2) {
|
|
generr(h, "%s:%d: missing shared secret", path,
|
|
linenum);
|
|
retval = -1;
|
|
break;
|
|
}
|
|
host = fields[0];
|
|
secret = fields[1];
|
|
|
|
/* Parse and validate the fields. */
|
|
res = host;
|
|
host = strsep(&res, ":");
|
|
port_str = strsep(&res, ":");
|
|
if (port_str != NULL) {
|
|
port = strtoul(port_str, &end, 10);
|
|
if (port_str[0] == '\0' || *end != '\0') {
|
|
generr(h, "%s:%d: invalid port", path,
|
|
linenum);
|
|
retval = -1;
|
|
break;
|
|
}
|
|
} else
|
|
port = 0;
|
|
i = 2;
|
|
if (nfields > i && strlen(fields[i]) > 0 &&
|
|
strspn(fields[i], "0123456789") == strlen(fields[i])) {
|
|
timeout_str = fields[i];
|
|
timeout = strtoul(timeout_str, &end, 10);
|
|
if (timeout_str[0] == '\0' || *end != '\0') {
|
|
generr(h, "%s:%d: invalid timeout", path,
|
|
linenum);
|
|
retval = -1;
|
|
break;
|
|
}
|
|
i++;
|
|
} else
|
|
timeout = TIMEOUT;
|
|
options = 0;
|
|
if (nfields > i &&
|
|
strcmp(fields[i], "single-connection") == 0) {
|
|
options |= TAC_SRVR_SINGLE_CONNECT;
|
|
i++;
|
|
}
|
|
if (tac_add_server_av(h, host, port, secret, timeout,
|
|
options, (const char *const *)(fields + i)) == -1) {
|
|
char msg[ERRSIZE];
|
|
|
|
strcpy(msg, h->errmsg);
|
|
generr(h, "%s:%d: %s", path, linenum, msg);
|
|
retval = -1;
|
|
break;
|
|
}
|
|
memset_s(secret, strlen(secret), 0, strlen(secret));
|
|
freev(fields, nfields);
|
|
}
|
|
freev(fields, nfields);
|
|
fclose(fp);
|
|
return retval;
|
|
}
|
|
|
|
int
|
|
tac_create_authen(struct tac_handle *h, int action, int type, int service)
|
|
{
|
|
struct tac_authen_start *as;
|
|
|
|
create_msg(h, TAC_AUTHEN, action, type);
|
|
|
|
as = &h->request.u.authen_start;
|
|
as->action = action;
|
|
as->priv_lvl = TAC_PRIV_LVL_USER;
|
|
as->authen_type = type;
|
|
as->service = service;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
tac_create_author(struct tac_handle *h, int method, int type, int service)
|
|
{
|
|
struct tac_author_request *areq;
|
|
|
|
create_msg(h, TAC_AUTHOR, method, type);
|
|
|
|
areq = &h->request.u.author_request;
|
|
areq->authen_meth = method;
|
|
areq->priv_lvl = TAC_PRIV_LVL_USER;
|
|
areq->authen_type = type;
|
|
areq->service = service;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
tac_create_acct(struct tac_handle *h, int acct, int action, int type, int service)
|
|
{
|
|
struct tac_acct_start *as;
|
|
|
|
create_msg(h, TAC_ACCT, action, type);
|
|
|
|
as = &h->request.u.acct_start;
|
|
as->action = acct;
|
|
as->authen_action = action;
|
|
as->priv_lvl = TAC_PRIV_LVL_USER;
|
|
as->authen_type = type;
|
|
as->authen_service = service;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
create_msg(struct tac_handle *h, int msg_type, int var, int type)
|
|
{
|
|
struct tac_msg *msg;
|
|
int i;
|
|
|
|
h->last_seq_no = 0;
|
|
|
|
msg = &h->request;
|
|
msg->type = msg_type;
|
|
msg->version = protocol_version(msg_type, var, type);
|
|
msg->flags = 0; /* encrypted packet body */
|
|
|
|
free_str(&h->user);
|
|
free_str(&h->port);
|
|
free_str(&h->rem_addr);
|
|
free_str(&h->data);
|
|
free_str(&h->user_msg);
|
|
|
|
for (i=0; i<MAXAVPAIRS; i++)
|
|
free_str(&(h->avs[i]));
|
|
}
|
|
|
|
void *
|
|
tac_get_data(struct tac_handle *h, size_t *len)
|
|
{
|
|
return dup_str(h, &h->srvr_data, len);
|
|
}
|
|
|
|
char *
|
|
tac_get_msg(struct tac_handle *h)
|
|
{
|
|
return dup_str(h, &h->srvr_msg, NULL);
|
|
}
|
|
|
|
/*
|
|
* Create and initialize a tac_handle structure, and return it to the
|
|
* caller. Can fail only if the necessary memory cannot be allocated.
|
|
* In that case, it returns NULL.
|
|
*/
|
|
struct tac_handle *
|
|
tac_open(void)
|
|
{
|
|
int i;
|
|
struct tac_handle *h;
|
|
|
|
h = (struct tac_handle *)malloc(sizeof(struct tac_handle));
|
|
if (h != NULL) {
|
|
h->fd = -1;
|
|
h->num_servers = 0;
|
|
h->cur_server = 0;
|
|
h->errmsg[0] = '\0';
|
|
init_str(&h->user);
|
|
init_str(&h->port);
|
|
init_str(&h->rem_addr);
|
|
init_str(&h->data);
|
|
init_str(&h->user_msg);
|
|
for (i=0; i<MAXAVPAIRS; i++) {
|
|
init_str(&(h->avs[i]));
|
|
init_str(&(h->srvr_avs[i]));
|
|
}
|
|
init_str(&h->srvr_msg);
|
|
init_str(&h->srvr_data);
|
|
}
|
|
return h;
|
|
}
|
|
|
|
int
|
|
tac_send_authen(struct tac_handle *h)
|
|
{
|
|
struct tac_authen_reply *ar;
|
|
|
|
if (h->num_servers == 0)
|
|
return -1;
|
|
|
|
if (h->last_seq_no == 0) { /* Authentication START packet */
|
|
struct tac_authen_start *as;
|
|
|
|
as = &h->request.u.authen_start;
|
|
h->request.length =
|
|
htonl(offsetof(struct tac_authen_start, rest[0]));
|
|
if (add_str_8(h, &as->user_len, &h->user) == -1 ||
|
|
add_str_8(h, &as->port_len, &h->port) == -1 ||
|
|
add_str_8(h, &as->rem_addr_len, &h->rem_addr) == -1 ||
|
|
add_str_8(h, &as->data_len, &h->data) == -1)
|
|
return -1;
|
|
} else { /* Authentication CONTINUE packet */
|
|
struct tac_authen_cont *ac;
|
|
|
|
ac = &h->request.u.authen_cont;
|
|
ac->flags = 0;
|
|
h->request.length =
|
|
htonl(offsetof(struct tac_authen_cont, rest[0]));
|
|
if (add_str_16(h, &ac->user_msg_len, &h->user_msg) == -1 ||
|
|
add_str_16(h, &ac->data_len, &h->data) == -1)
|
|
return -1;
|
|
}
|
|
|
|
/* Send the message and retrieve the reply. */
|
|
if (send_msg(h) == -1 || recv_msg(h) == -1)
|
|
return -1;
|
|
|
|
/* Scan the optional fields in the reply. */
|
|
ar = &h->response.u.authen_reply;
|
|
h->srvr_pos = offsetof(struct tac_authen_reply, rest[0]);
|
|
if (get_str(h, "msg", &h->srvr_msg, ntohs(ar->msg_len)) == -1 ||
|
|
get_str(h, "data", &h->srvr_data, ntohs(ar->data_len)) == -1 ||
|
|
get_srvr_end(h) == -1)
|
|
return -1;
|
|
|
|
if (!h->single_connect &&
|
|
ar->status != TAC_AUTHEN_STATUS_GETDATA &&
|
|
ar->status != TAC_AUTHEN_STATUS_GETUSER &&
|
|
ar->status != TAC_AUTHEN_STATUS_GETPASS)
|
|
close_connection(h);
|
|
|
|
return ar->flags << 8 | ar->status;
|
|
}
|
|
|
|
int
|
|
tac_send_author(struct tac_handle *h)
|
|
{
|
|
int i, current;
|
|
char dbgstr[64];
|
|
struct tac_author_request *areq = &h->request.u.author_request;
|
|
struct tac_author_response *ares = &h->response.u.author_response;
|
|
struct tac_server *srvp;
|
|
|
|
h->request.length =
|
|
htonl(offsetof(struct tac_author_request, rest[0]));
|
|
|
|
/* Count each specified AV pair */
|
|
for (areq->av_cnt=0, i=0; i<MAXAVPAIRS; i++)
|
|
if (h->avs[i].len && h->avs[i].data)
|
|
areq->av_cnt++;
|
|
|
|
/*
|
|
* Each AV size is a byte starting right after 'av_cnt'. Update the
|
|
* offset to include these AV sizes.
|
|
*/
|
|
h->request.length = ntohl(htonl(h->request.length) + areq->av_cnt);
|
|
|
|
/* Now add the string arguments from 'h' */
|
|
if (add_str_8(h, &areq->user_len, &h->user) == -1 ||
|
|
add_str_8(h, &areq->port_len, &h->port) == -1 ||
|
|
add_str_8(h, &areq->rem_addr_len, &h->rem_addr) == -1)
|
|
return -1;
|
|
|
|
/* Add each AV pair, the size of each placed in areq->rest[current] */
|
|
for (current=0, i=0; i<MAXAVPAIRS; i++) {
|
|
if (h->avs[i].len && h->avs[i].data) {
|
|
if (add_str_8(h, &areq->rest[current++],
|
|
&(h->avs[i])) == -1)
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/* Send the message and retrieve the reply. */
|
|
if (send_msg(h) == -1 || recv_msg(h) == -1)
|
|
return -1;
|
|
srvp = &h->servers[h->cur_server];
|
|
|
|
/* Update the offset in the response packet based on av pairs count */
|
|
h->srvr_pos = offsetof(struct tac_author_response, rest[0]) +
|
|
ares->av_cnt;
|
|
|
|
/* Scan the optional fields in the response. */
|
|
if (get_str(h, "msg", &h->srvr_msg, ntohs(ares->msg_len)) == -1 ||
|
|
get_str(h, "data", &h->srvr_data, ntohs(ares->data_len)) ==-1)
|
|
return -1;
|
|
|
|
/* Get each AV pair (just setting pointers, not malloc'ing) */
|
|
clear_srvr_avs(h);
|
|
for (i=0; i<ares->av_cnt; i++) {
|
|
snprintf(dbgstr, sizeof dbgstr, "av-pair-%d", i);
|
|
if (get_str(h, dbgstr, &(h->srvr_avs[i]),
|
|
ares->rest[i]) == -1)
|
|
return -1;
|
|
h->srvr_navs++;
|
|
}
|
|
|
|
/* Should have ended up at the end */
|
|
if (get_srvr_end(h) == -1)
|
|
return -1;
|
|
|
|
/* Sanity checks */
|
|
if (!h->single_connect)
|
|
close_connection(h);
|
|
|
|
return (h->srvr_navs + srvp->navs) << 8 | ares->status;
|
|
}
|
|
|
|
int
|
|
tac_send_acct(struct tac_handle *h)
|
|
{
|
|
register int i, current;
|
|
struct tac_acct_start *as = &h->request.u.acct_start;
|
|
struct tac_acct_reply *ar = &h->response.u.acct_reply;
|
|
|
|
/* start */
|
|
as = &h->request.u.acct_start;
|
|
h->request.length = htonl(offsetof(struct tac_acct_start, rest[0]));
|
|
for (as->av_cnt = 0, i = 0; i < MAXAVPAIRS; i++)
|
|
if (h->avs[i].len && h->avs[i].data)
|
|
as->av_cnt++;
|
|
h->request.length = ntohl(htonl(h->request.length) + as->av_cnt);
|
|
|
|
if (add_str_8(h, &as->user_len, &h->user) == -1 ||
|
|
add_str_8(h, &as->port_len, &h->port) == -1 ||
|
|
add_str_8(h, &as->rem_addr_len, &h->rem_addr) == -1)
|
|
return -1;
|
|
|
|
for (i = current = 0; i < MAXAVPAIRS; i++)
|
|
if (h->avs[i].len && h->avs[i].data)
|
|
if (add_str_8(h, &as->rest[current++], &(h->avs[i])) == -1)
|
|
return -1;
|
|
|
|
/* send */
|
|
if (send_msg(h) == -1 || recv_msg(h) == -1)
|
|
return -1;
|
|
|
|
/* reply */
|
|
h->srvr_pos = offsetof(struct tac_acct_reply, rest[0]);
|
|
if (get_str(h, "msg", &h->srvr_msg, ntohs(ar->msg_len)) == -1 ||
|
|
get_str(h, "data", &h->srvr_data, ntohs(ar->data_len)) == -1 ||
|
|
get_srvr_end(h) == -1)
|
|
return -1;
|
|
|
|
/* Sanity checks */
|
|
if (!h->single_connect)
|
|
close_connection(h);
|
|
|
|
return ar->status;
|
|
}
|
|
|
|
int
|
|
tac_set_rem_addr(struct tac_handle *h, const char *addr)
|
|
{
|
|
return save_str(h, &h->rem_addr, addr, addr != NULL ? strlen(addr) : 0);
|
|
}
|
|
|
|
int
|
|
tac_set_data(struct tac_handle *h, const void *data, size_t data_len)
|
|
{
|
|
return save_str(h, &h->data, data, data_len);
|
|
}
|
|
|
|
int
|
|
tac_set_msg(struct tac_handle *h, const char *msg)
|
|
{
|
|
return save_str(h, &h->user_msg, msg, msg != NULL ? strlen(msg) : 0);
|
|
}
|
|
|
|
int
|
|
tac_set_port(struct tac_handle *h, const char *port)
|
|
{
|
|
return save_str(h, &h->port, port, port != NULL ? strlen(port) : 0);
|
|
}
|
|
|
|
int
|
|
tac_set_priv(struct tac_handle *h, int priv)
|
|
{
|
|
if (!(TAC_PRIV_LVL_MIN <= priv && priv <= TAC_PRIV_LVL_MAX)) {
|
|
generr(h, "Attempt to set invalid privilege level");
|
|
return -1;
|
|
}
|
|
h->request.u.authen_start.priv_lvl = priv;
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
tac_set_user(struct tac_handle *h, const char *user)
|
|
{
|
|
return save_str(h, &h->user, user, user != NULL ? strlen(user) : 0);
|
|
}
|
|
|
|
int
|
|
tac_set_av(struct tac_handle *h, u_int index, const char *av)
|
|
{
|
|
if (index >= MAXAVPAIRS)
|
|
return -1;
|
|
return save_str(h, &(h->avs[index]), av, av != NULL ? strlen(av) : 0);
|
|
}
|
|
|
|
char *
|
|
tac_get_av(struct tac_handle *h, u_int index)
|
|
{
|
|
struct tac_server *srvp;
|
|
|
|
if (index < h->srvr_navs)
|
|
return dup_str(h, &h->srvr_avs[index], NULL);
|
|
index -= h->srvr_navs;
|
|
srvp = &h->servers[h->cur_server];
|
|
if (index < srvp->navs)
|
|
return xstrdup(h, srvp->avs[index].data);
|
|
return NULL;
|
|
}
|
|
|
|
char *
|
|
tac_get_av_value(struct tac_handle *h, const char *attribute)
|
|
{
|
|
int i, attr_len;
|
|
int found_seperator;
|
|
char *ch, *end;
|
|
struct tac_str *candidate;
|
|
struct tac_str value;
|
|
struct tac_server *srvp = &h->servers[h->cur_server];
|
|
|
|
if (attribute == NULL || (attr_len = strlen(attribute)) == 0)
|
|
return NULL;
|
|
|
|
for (i = 0; i < h->srvr_navs + srvp->navs; i++) {
|
|
if (i < h->srvr_navs)
|
|
candidate = &h->srvr_avs[i];
|
|
else
|
|
candidate = &srvp->avs[i - h->srvr_navs];
|
|
|
|
if (attr_len < candidate->len &&
|
|
strncmp(candidate->data, attribute, attr_len) == 0) {
|
|
|
|
ch = candidate->data + attr_len;
|
|
end = candidate->data + candidate->len;
|
|
|
|
/*
|
|
* Sift out the white space between A and V (should not
|
|
* be any, but don't trust implementation of server...)
|
|
*/
|
|
found_seperator = 0;
|
|
while ((*ch == '=' || *ch == '*' || *ch == ' ' ||
|
|
*ch == '\t') && ch != end) {
|
|
if (*ch == '=' || *ch == '*')
|
|
found_seperator++;
|
|
ch++;
|
|
}
|
|
|
|
/*
|
|
* Note:
|
|
* The case of 'attribute' == "foo" and
|
|
* h->srvr_avs[0] = "foobie=var1"
|
|
* h->srvr_avs[1] = "foo=var2"
|
|
* is handled.
|
|
*
|
|
* Note that for empty string attribute values a
|
|
* 0-length string is returned in order to distinguish
|
|
* against unset values.
|
|
* dup_str() will handle srvr.len == 0 correctly.
|
|
*/
|
|
if (found_seperator == 1) {
|
|
value.len = end - ch;
|
|
value.data = ch;
|
|
return dup_str(h, &value, NULL);
|
|
}
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
void
|
|
tac_clear_avs(struct tac_handle *h)
|
|
{
|
|
int i;
|
|
for (i=0; i<MAXAVPAIRS; i++)
|
|
save_str(h, &(h->avs[i]), NULL, 0);
|
|
}
|
|
|
|
static void
|
|
clear_srvr_avs(struct tac_handle *h)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < h->srvr_navs; i++)
|
|
init_str(&(h->srvr_avs[i]));
|
|
h->srvr_navs = 0;
|
|
}
|
|
|
|
|
|
const char *
|
|
tac_strerror(struct tac_handle *h)
|
|
{
|
|
return h->errmsg;
|
|
}
|
|
|
|
static void *
|
|
xmalloc(struct tac_handle *h, size_t size)
|
|
{
|
|
void *r;
|
|
|
|
if ((r = malloc(size)) == NULL)
|
|
generr(h, "Out of memory");
|
|
return r;
|
|
}
|
|
|
|
static char *
|
|
xstrdup(struct tac_handle *h, const char *s)
|
|
{
|
|
char *r;
|
|
|
|
if ((r = strdup(s)) == NULL)
|
|
generr(h, "Out of memory");
|
|
return r;
|
|
}
|