summaryrefslogtreecommitdiff
path: root/usr.sbin/radiusctl/radiusctl.c
diff options
context:
space:
mode:
authorYASUOKA Masahiko <yasuoka@cvs.openbsd.org>2024-07-09 17:26:15 +0000
committerYASUOKA Masahiko <yasuoka@cvs.openbsd.org>2024-07-09 17:26:15 +0000
commit8f03aebd6993a64b4fbe441add0c08118d08f7ba (patch)
tree1663751ed62962dff80fdf55af493618db6b769e /usr.sbin/radiusctl/radiusctl.c
parent6642cc19c55dcfb391391800d361ade8788b7c60 (diff)
Add radiusd_ipcp(8). A module which provides IP configuration through
RADIUS Access-Accept messages and manages IP address pool through RADIUS accounting messages.
Diffstat (limited to 'usr.sbin/radiusctl/radiusctl.c')
-rw-r--r--usr.sbin/radiusctl/radiusctl.c572
1 files changed, 551 insertions, 21 deletions
diff --git a/usr.sbin/radiusctl/radiusctl.c b/usr.sbin/radiusctl/radiusctl.c
index c374884eb01..8c46815c0de 100644
--- a/usr.sbin/radiusctl/radiusctl.c
+++ b/usr.sbin/radiusctl/radiusctl.c
@@ -1,4 +1,4 @@
-/* $OpenBSD: radiusctl.c,v 1.8 2020/02/24 07:07:11 dlg Exp $ */
+/* $OpenBSD: radiusctl.c,v 1.9 2024/07/09 17:26:14 yasuoka Exp $ */
/*
* Copyright (c) 2015 YASUOKA Masahiko <yasuoka@yasuoka.net>
*
@@ -15,33 +15,73 @@
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <sys/types.h>
+#include <sys/cdefs.h>
#include <sys/socket.h>
+#include <sys/time.h>
+#include <sys/uio.h>
+#include <sys/un.h>
#include <netinet/in.h>
-
#include <arpa/inet.h>
-#include <errno.h>
+
#include <err.h>
+#include <errno.h>
+#include <event.h>
+#include <imsg.h>
+#include <inttypes.h>
#include <md5.h>
#include <netdb.h>
+#include <radius.h>
#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
+#include <sysexits.h>
+#include <time.h>
#include <unistd.h>
-#include <radius.h>
-
-#include <event.h>
-
#include "parser.h"
+#include "radiusd.h"
+#include "radiusd_ipcp.h"
#include "chap_ms.h"
+#include "json.h"
+#ifndef MAXIMUM
+#define MAXIMUM(_a, _b) (((_a) > (_b))? (_a) : (_b))
+#endif
-static int radius_test (struct parse_result *);
-static void radius_dump (FILE *, RADIUS_PACKET *, bool,
+static int radius_test(struct parse_result *);
+static void radius_dump(FILE *, RADIUS_PACKET *, bool,
const char *);
-static const char *radius_code_str (int code);
+
+static int ipcp_handle_imsg(struct parse_result *, struct imsg *,
+ int);
+static void ipcp_handle_show(struct radiusd_ipcp_db_dump *,
+ size_t, int);
+static void ipcp_handle_dumps(struct radiusd_ipcp_db_dump *,
+ size_t, int);
+static void ipcp_handle_dump(struct radiusd_ipcp_db_dump *,
+ size_t, int);
+static void ipcp_handle_dump0(struct radiusd_ipcp_db_dump *,
+ size_t, struct timespec *, struct timespec *,
+ struct timespec *, int);
+static void ipcp_handle_stat(struct radiusd_ipcp_statistics *);
+static void ipcp_handle_jsons(struct radiusd_ipcp_db_dump *,
+ size_t, int);
+static void ipcp_handle_json(struct radiusd_ipcp_db_dump *,
+ size_t, struct radiusd_ipcp_statistics *, int);
+static void ipcp_handle_json0(struct radiusd_ipcp_db_dump *,
+ size_t, struct timespec *, struct timespec *,
+ struct timespec *, int);
+
+static const char *radius_code_str(int code);
static const char *hexstr(const u_char *, int, char *, int);
+static const char *sockaddr_str(struct sockaddr *, char *, size_t);
+static const char *time_long_str(struct timespec *, char *, size_t);
+static const char *time_short_str(struct timespec *, struct timespec *,
+ char *, size_t);
+static const char *humanize_seconds(long, char *, size_t);
static void
usage(void)
@@ -54,9 +94,15 @@ usage(void)
int
main(int argc, char *argv[])
{
- int ch;
- struct parse_result *result;
- int ecode = EXIT_SUCCESS;
+ int ch, sock, done = 0;
+ ssize_t n;
+ struct parse_result *res;
+ struct sockaddr_un sun;
+ struct imsgbuf ibuf;
+ struct imsg imsg;
+ struct iovec iov[5];
+ int niov = 0, cnt = 0;
+ char module_name[RADIUSD_MODULE_NAME_LEN + 1];
while ((ch = getopt(argc, argv, "")) != -1)
switch (ch) {
@@ -67,22 +113,112 @@ main(int argc, char *argv[])
argc -= optind;
argv += optind;
- if ((result = parse(argc, argv)) == NULL)
- return (EXIT_FAILURE);
+ if (unveil(RADIUSD_SOCK, "rw") == -1)
+ err(EX_OSERR, "unveil");
+ if (pledge("stdio unix rpath dns inet", NULL) == -1)
+ err(EX_OSERR, "pledge");
+
+ res = parse(argc, argv);
+ if (res == NULL)
+ exit(EX_USAGE);
- switch (result->action) {
+ switch (res->action) {
+ default:
+ break;
case NONE:
+ exit(EXIT_SUCCESS);
break;
case TEST:
if (pledge("stdio dns inet", NULL) == -1)
err(EXIT_FAILURE, "pledge");
- ecode = radius_test(result);
+ exit(radius_test(res));
+ break;
+ }
+
+ if (pledge("stdio unix rpath", NULL) == -1)
+ err(EX_OSERR, "pledge");
+
+ memset(&sun, 0, sizeof(sun));
+ sun.sun_family = AF_UNIX;
+ sun.sun_len = sizeof(sun);
+ strlcpy(sun.sun_path, RADIUSD_SOCK, sizeof(sun.sun_path));
+
+ if ((sock = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
+ err(EX_OSERR, "socket");
+ if (connect(sock, (struct sockaddr *)&sun, sizeof(sun)) == -1)
+ err(EX_OSERR, "connect");
+ imsg_init(&ibuf, sock);
+
+ res = parse(argc, argv);
+ if (res == NULL)
+ exit(EX_USAGE);
+
+ switch (res->action) {
+ case TEST:
+ case NONE:
+ abort();
+ break;
+ case IPCP_SHOW:
+ case IPCP_DUMP:
+ case IPCP_MONITOR:
+ memset(module_name, 0, sizeof(module_name));
+ strlcpy(module_name, "ipcp",
+ sizeof(module_name));
+ iov[niov].iov_base = module_name;
+ iov[niov++].iov_len = RADIUSD_MODULE_NAME_LEN;
+ imsg_composev(&ibuf, (res->action == IPCP_MONITOR)?
+ IMSG_RADIUSD_MODULE_IPCP_MONITOR :
+ IMSG_RADIUSD_MODULE_IPCP_DUMP, 0, 0, -1, iov, niov);
break;
+ case IPCP_DISCONNECT:
+ memset(module_name, 0, sizeof(module_name));
+ strlcpy(module_name, "ipcp",
+ sizeof(module_name));
+ iov[niov].iov_base = module_name;
+ iov[niov++].iov_len = RADIUSD_MODULE_NAME_LEN;
+ iov[niov].iov_base = &res->session_seq;
+ iov[niov++].iov_len = sizeof(res->session_seq);
+ imsg_composev(&ibuf, IMSG_RADIUSD_MODULE_IPCP_DISCONNECT, 0, 0,
+ -1, iov, niov);
+ done = 1;
+ break;
+ }
+ while (ibuf.w.queued) {
+ if (msgbuf_write(&ibuf.w) <= 0 && errno != EAGAIN)
+ err(1, "ibuf_ctl: msgbuf_write error");
}
+ while (!done) {
+ if (((n = imsg_read(&ibuf)) == -1 && errno != EAGAIN) || n == 0)
+ break;
+ for (;;) {
+ if ((n = imsg_get(&ibuf, &imsg)) <= 0) {
+ if (n != 0)
+ done = 1;
+ break;
+ }
+ switch (res->action) {
+ case IPCP_SHOW:
+ case IPCP_DUMP:
+ case IPCP_MONITOR:
+ done = ipcp_handle_imsg(res, &imsg, cnt++);
+ break;
+ default:
+ break;
+ }
+ imsg_free(&imsg);
+ if (done)
+ break;
- return (ecode);
+ }
+ }
+ close(sock);
+
+ exit(EXIT_SUCCESS);
}
+/***********************************************************************
+ * "test"
+ ***********************************************************************/
struct radius_test {
const struct parse_result *res;
int ecode;
@@ -239,7 +375,7 @@ radius_test(struct parse_result *res)
test.res = res;
test.sock = sock;
test.reqpkt = reqpkt;
-
+
event_set(&test.ev_recv, sock, EV_READ|EV_PERSIST,
radius_test_recv, &test);
@@ -443,7 +579,6 @@ radius_dump(FILE *out, RADIUS_PACKET *pkt, bool resp, const char *secret)
fprintf(out, " MS-MPPE-Encryption-Policy = 0x%08x\n",
ntohl(*(u_long *)buf));
-
memset(buf, 0, sizeof(buf));
len = sizeof(buf);
if (radius_get_vs_raw_attr(pkt, RADIUS_VENDOR_MICROSOFT,
@@ -476,7 +611,315 @@ radius_dump(FILE *out, RADIUS_PACKET *pkt, bool resp, const char *secret)
}
-static const char *
+/***********************************************************************
+ * ipcp
+ ***********************************************************************/
+int
+ipcp_handle_imsg(struct parse_result *res, struct imsg *imsg, int cnt)
+{
+ ssize_t datalen;
+ struct radiusd_ipcp_db_dump *dump;
+ struct radiusd_ipcp_statistics *stat;
+ int done = 0;
+
+ datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+ switch (imsg->hdr.type) {
+ case IMSG_NG:
+ if (datalen > 0 && *((char *)imsg->data + datalen - 1) == '\0')
+ fprintf(stderr, "error: %s\n", (char *)imsg->data);
+ else
+ fprintf(stderr, "error\n");
+ exit(EXIT_FAILURE);
+ case IMSG_RADIUSD_MODULE_IPCP_DUMP:
+ if ((size_t)datalen < sizeof(struct
+ radiusd_ipcp_db_dump))
+ errx(1, "received a message which size is invalid");
+ dump = imsg->data;
+ if (res->action == IPCP_SHOW)
+ ipcp_handle_show(dump, datalen, (cnt++ == 0)? 1 : 0);
+ else {
+ if (res->flags & FLAGS_JSON)
+ ipcp_handle_jsons(dump, datalen,
+ (cnt++ == 0)? 1 : 0);
+ else
+ ipcp_handle_dumps(dump, datalen,
+ (cnt++ == 0)? 1 : 0);
+ }
+ if (dump->islast &&
+ (res->action == IPCP_SHOW || res->action == IPCP_DUMP))
+ done = 1;
+ break;
+ case IMSG_RADIUSD_MODULE_IPCP_START:
+ if ((size_t)datalen < offsetof(struct
+ radiusd_ipcp_db_dump, records[1]))
+ errx(1, "received a message which size is invalid");
+ dump = imsg->data;
+ if (res->flags & FLAGS_JSON)
+ ipcp_handle_json(dump, datalen, NULL, 0);
+ else {
+ printf("Start\n");
+ ipcp_handle_dump(dump, datalen, 0);
+ }
+ break;
+ case IMSG_RADIUSD_MODULE_IPCP_STOP:
+ if ((size_t)datalen < offsetof(
+ struct radiusd_ipcp_db_dump,
+ records[1]) +
+ sizeof(struct
+ radiusd_ipcp_statistics))
+ errx(1, "received a message which size is invalid");
+ dump = imsg->data;
+ stat = (struct radiusd_ipcp_statistics *)
+ ((char *)imsg->data + offsetof(
+ struct radiusd_ipcp_db_dump, records[1]));
+ if (res->flags & FLAGS_JSON)
+ ipcp_handle_json(dump, datalen, stat, 0);
+ else {
+ printf("Stop\n");
+ ipcp_handle_dump(dump, datalen, 0);
+ ipcp_handle_stat(stat);
+ }
+ break;
+ }
+
+ return (done);
+}
+
+static void
+ipcp_handle_show(struct radiusd_ipcp_db_dump *dump, size_t dumpsiz, int first)
+{
+ int i, width;
+ uint32_t maxseq = 999;
+ char buf0[128], buf1[NI_MAXHOST + NI_MAXSERV + 4], buf2[80];
+ struct timespec upt, now, dif, start;
+
+ clock_gettime(CLOCK_BOOTTIME, &upt);
+ clock_gettime(CLOCK_REALTIME, &now);
+ timespecsub(&now, &upt, &upt);
+
+ for (i = 0; ; i++) {
+ if (offsetof(struct radiusd_ipcp_db_dump, records[i])
+ >= dumpsiz)
+ break;
+ maxseq = MAXIMUM(maxseq, dump->records[i].rec.seq);
+ }
+ for (width = 0; maxseq != 0; maxseq /= 10, width++)
+ ;
+
+ for (i = 0; ; i++) {
+ if (offsetof(struct radiusd_ipcp_db_dump, records[i])
+ >= dumpsiz)
+ break;
+ if (i == 0 && first)
+ printf("%-*s Assigned Username "
+ "Start Tunnel From\n"
+ "%.*s --------------- ---------------------- "
+ "-------- %.*s\n", width, "Seq", width,
+ "----------", 28 - width,
+ "-------------------------");
+ timespecadd(&upt, &dump->records[i].rec.start, &start);
+ timespecsub(&now, &start, &dif);
+ printf("%*d %-15s %-22s %-8s %s\n",
+ width, dump->records[i].rec.seq,
+ inet_ntop(dump->records[i].af, &dump->records[i].addr,
+ buf0, sizeof(buf0)), dump->records[i].rec.username,
+ time_short_str(&start, &dif, buf2, sizeof(buf2)),
+ sockaddr_str(
+ (struct sockaddr *)&dump->records[i].rec.tun_client, buf1,
+ sizeof(buf1)));
+ }
+}
+static void
+ipcp_handle_dump(struct radiusd_ipcp_db_dump *dump, size_t dumpsiz, int idx)
+{
+ struct timespec upt, now, dif, start, timeout;
+
+ clock_gettime(CLOCK_BOOTTIME, &upt);
+ clock_gettime(CLOCK_REALTIME, &now);
+ timespecsub(&now, &upt, &upt);
+
+ timespecadd(&upt, &dump->records[idx].rec.start, &start);
+ timespecsub(&now, &start, &dif);
+
+ if (dump->records[idx].rec.start.tv_sec == 0)
+ ipcp_handle_dump0(dump, dumpsiz, &dif, &start, NULL, idx);
+ else {
+ timespecadd(&upt, &dump->records[idx].rec.timeout, &timeout);
+ ipcp_handle_dump0(dump, dumpsiz, &dif, &start, &timeout, idx);
+ }
+}
+
+static void
+ipcp_handle_dump0(struct radiusd_ipcp_db_dump *dump, size_t dumpsiz,
+ struct timespec *dif, struct timespec *start, struct timespec *timeout,
+ int idx)
+{
+ char buf0[128], buf1[NI_MAXHOST + NI_MAXSERV + 4], buf2[80];
+
+ printf(
+ " Sequence Number : %u\n"
+ " Session Id : %s\n"
+ " Username : %s\n"
+ " Auth Method : %s\n"
+ " Assigned IP Address : %s\n"
+ " Start Time : %s\n"
+ " Elapsed Time : %lld second%s%s\n",
+ dump->records[idx].rec.seq, dump->records[idx].rec.session_id,
+ dump->records[idx].rec.username, dump->records[idx].rec.auth_method,
+ inet_ntop(dump->records[idx].af, &dump->records[idx].addr, buf0,
+ sizeof(buf0)), time_long_str(start, buf1, sizeof(buf1)),
+ (long long)dif->tv_sec, (dif->tv_sec == 0)? "" : "s",
+ humanize_seconds(dif->tv_sec, buf2, sizeof(buf2)));
+ if (timeout != NULL)
+ printf(" Timeout : %s\n",
+ time_long_str(timeout, buf0, sizeof(buf0)));
+ printf(
+ " NAS Identifier : %s\n"
+ " Tunnel Type : %s\n"
+ " Tunnel From : %s\n",
+ dump->records[idx].rec.nas_id, dump->records[idx].rec.tun_type,
+ sockaddr_str((struct sockaddr *)
+ &dump->records[idx].rec.tun_client, buf1, sizeof(buf1)));
+}
+
+void
+ipcp_handle_stat(struct radiusd_ipcp_statistics *stat)
+{
+ printf(
+ " Terminate Cause : %s\n"
+ " Input Packets : %"PRIu32"\n"
+ " Output Packets : %"PRIu32"\n"
+ " Input Bytes : %"PRIu64"\n"
+ " Output Bytes : %"PRIu64"\n",
+ stat->cause, stat->ipackets, stat->opackets, stat->ibytes,
+ stat->obytes);
+}
+
+static void
+ipcp_handle_jsons(struct radiusd_ipcp_db_dump *dump, size_t dumpsiz, int first)
+{
+ int i;
+ struct timespec upt, now, dif, start, timeout;
+
+ clock_gettime(CLOCK_BOOTTIME, &upt);
+ clock_gettime(CLOCK_REALTIME, &now);
+ timespecsub(&now, &upt, &upt);
+
+ for (i = 0; ; i++) {
+ if (offsetof(struct radiusd_ipcp_db_dump, records[i])
+ >= dumpsiz)
+ break;
+ timespecadd(&upt, &dump->records[i].rec.start, &start);
+ timespecsub(&now, &start, &dif);
+ json_do_start(stdout);
+ json_do_string("action", "start");
+ if (dump->records[i].rec.timeout.tv_sec == 0)
+ ipcp_handle_json0(dump, dumpsiz, &dif, &start, NULL, i);
+ else {
+ timespecadd(&upt, &dump->records[i].rec.timeout,
+ &timeout);
+ ipcp_handle_json0(dump, dumpsiz, &dif, &start, &timeout,
+ i);
+ }
+ json_do_finish();
+ }
+ fflush(stdout);
+}
+
+static void
+ipcp_handle_json(struct radiusd_ipcp_db_dump *dump, size_t dumpsiz,
+ struct radiusd_ipcp_statistics *stat, int idx)
+{
+ struct timespec upt, now, dif, start, timeout;
+
+ json_do_start(stdout);
+ clock_gettime(CLOCK_BOOTTIME, &upt);
+ clock_gettime(CLOCK_REALTIME, &now);
+ timespecsub(&now, &upt, &upt);
+ timespecadd(&upt, &dump->records[idx].rec.start, &start);
+ timespecsub(&now, &start, &dif);
+
+ if (stat == NULL)
+ json_do_string("action", "start");
+ else
+ json_do_string("action", "stop");
+ if (dump->records[idx].rec.timeout.tv_sec == 0)
+ ipcp_handle_json0(dump, dumpsiz, &dif, &start, NULL, idx);
+ else {
+ timespecadd(&upt, &dump->records[idx].rec.timeout, &timeout);
+ ipcp_handle_json0(dump, dumpsiz, &dif, &start, &timeout, idx);
+ }
+ if (stat != NULL) {
+ json_do_string("terminate-cause", stat->cause);
+ json_do_uint("input-packets", stat->ipackets);
+ json_do_uint("output-packets", stat->opackets);
+ json_do_uint("input-bytes", stat->ibytes);
+ json_do_uint("output-bytes", stat->obytes);
+ }
+ json_do_finish();
+ fflush(stdout);
+}
+
+static void
+ipcp_handle_json0(struct radiusd_ipcp_db_dump *dump, size_t dumpsiz,
+ struct timespec *dif, struct timespec *start, struct timespec *timeout,
+ int idx)
+{
+ char buf[128];
+
+ json_do_uint("sequence-number", dump->records[idx].rec.seq);
+ json_do_string("session-id", dump->records[idx].rec.session_id);
+ json_do_string("username", dump->records[idx].rec.username);
+ json_do_string("auth-method", dump->records[idx].rec.auth_method);
+ json_do_string("assigned-ip-address", inet_ntop(dump->records[idx].af,
+ &dump->records[idx].addr, buf, sizeof(buf)));
+ json_do_uint("start", start->tv_sec);
+ json_do_uint("elapsed", dif->tv_sec);
+ if (timeout != NULL)
+ json_do_uint("timeout", timeout->tv_sec);
+ json_do_string("nas-identifier", dump->records[idx].rec.nas_id);
+ json_do_string("tunnel-type", dump->records[idx].rec.tun_type);
+ json_do_string("tunnel-from",
+ sockaddr_str((struct sockaddr *)&dump->records[idx].rec.tun_client,
+ buf, sizeof(buf)));
+}
+
+static void
+ipcp_handle_dumps(struct radiusd_ipcp_db_dump *dump, size_t dumpsiz, int first)
+{
+ static int cnt = 0;
+ int i;
+ struct timespec upt, now, dif, start, timeout;
+
+ clock_gettime(CLOCK_BOOTTIME, &upt);
+ clock_gettime(CLOCK_REALTIME, &now);
+ timespecsub(&now, &upt, &upt);
+
+ if (first)
+ cnt = 0;
+ for (i = 0; ; i++, cnt++) {
+ if (offsetof(struct radiusd_ipcp_db_dump, records[i])
+ >= dumpsiz)
+ break;
+ timespecadd(&upt, &dump->records[i].rec.start, &start);
+ timespecsub(&now, &start, &dif);
+ printf("#%d\n", cnt + 1);
+ if (dump->records[i].rec.timeout.tv_sec == 0)
+ ipcp_handle_dump0(dump, dumpsiz, &dif, &start, NULL, i);
+ else {
+ timespecadd(&upt, &dump->records[i].rec.timeout,
+ &timeout);
+ ipcp_handle_dump0(dump, dumpsiz, &dif, &start,
+ &timeout, i);
+ }
+ }
+}
+
+
+/***********************************************************************
+ * Miscellaneous functions
+ ***********************************************************************/
+const char *
radius_code_str(int code)
{
int i;
@@ -523,3 +966,90 @@ hexstr(const u_char *data, int len, char *str, int strsiz)
return (str);
}
+
+const char *
+sockaddr_str(struct sockaddr *sa, char *buf, size_t bufsiz)
+{
+ int noport, ret;
+ char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];
+
+ if (ntohs(((struct sockaddr_in *)sa)->sin_port) == 0) {
+ noport = 1;
+ ret = getnameinfo(sa, sa->sa_len, hbuf, sizeof(hbuf), NULL, 0,
+ NI_NUMERICHOST);
+ } else {
+ noport = 0;
+ ret = getnameinfo(sa, sa->sa_len, hbuf, sizeof(hbuf), sbuf,
+ sizeof(sbuf), NI_NUMERICHOST | NI_NUMERICSERV);
+ }
+ if (ret != 0)
+ return "";
+ if (noport)
+ strlcpy(buf, hbuf, bufsiz);
+ else if (sa->sa_family == AF_INET6)
+ snprintf(buf, bufsiz, "[%s]:%s", hbuf, sbuf);
+ else
+ snprintf(buf, bufsiz, "%s:%s", hbuf, sbuf);
+
+ return (buf);
+}
+
+const char *
+time_long_str(struct timespec *tim, char *buf, size_t bufsiz)
+{
+ struct tm tm;
+
+ localtime_r(&tim->tv_sec, &tm);
+ strftime(buf, bufsiz, "%F %T", &tm);
+
+ return (buf);
+}
+
+const char *
+time_short_str(struct timespec *tim, struct timespec *dif, char *buf,
+ size_t bufsiz)
+{
+ struct tm tm;
+
+ localtime_r(&tim->tv_sec, &tm);
+ if (dif->tv_sec < 12 * 60 * 60)
+ strftime(buf, bufsiz, "%l:%M%p", &tm);
+ else if (dif->tv_sec < 7 * 24 * 60 * 60)
+ strftime(buf, bufsiz, "%e%b%y", &tm);
+ else
+ strftime(buf, bufsiz, "%m/%d", &tm);
+
+ return (buf);
+}
+
+const char *
+humanize_seconds(long seconds, char *buf, size_t bufsiz)
+{
+ char fbuf[80];
+ int hour, min;
+
+ hour = seconds / 3600;
+ min = (seconds % 3600) / 60;
+
+ if (bufsiz == 0)
+ return NULL;
+ buf[0] = '\0';
+ if (hour != 0 || min != 0) {
+ strlcat(buf, " (", bufsiz);
+ if (hour != 0) {
+ snprintf(fbuf, sizeof(fbuf), "%d hour%s", hour,
+ (hour == 1)? "" : "s");
+ strlcat(buf, fbuf, bufsiz);
+ }
+ if (hour != 0 && min != 0)
+ strlcat(buf, " and ", bufsiz);
+ if (min != 0) {
+ snprintf(fbuf, sizeof(fbuf), "%d minute%s", min,
+ (min == 1)? "" : "s");
+ strlcat(buf, fbuf, bufsiz);
+ }
+ strlcat(buf, ")", bufsiz);
+ }
+
+ return (buf);
+}