1 /* Copyright (C) 2019  C. McEnroe <june@causal.agency>
2  *
3  * This program is free software: you can redistribute it and/or modify
4  * it under the terms of the GNU General Public License as published by
5  * the Free Software Foundation, either version 3 of the License, or
6  * (at your option) any later version.
7  *
8  * This program is distributed in the hope that it will be useful,
9  * but WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11  * GNU General Public License for more details.
12  *
13  * You should have received a copy of the GNU General Public License
14  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
15  *
16  * Additional permission under GNU GPL version 3 section 7:
17  *
18  * If you modify this Program, or any covered work, by linking or
19  * combining it with OpenSSL (or a modified version of that library),
20  * containing parts covered by the terms of the OpenSSL License and the
21  * original SSLeay license, the licensors of this Program grant you
22  * additional permission to convey the resulting work. Corresponding
23  * Source for a non-source form of such a combination shall include the
24  * source code for the parts of OpenSSL used as well as that of the
25  * covered work.
26  */
27 
28 #include <assert.h>
29 #include <err.h>
30 #include <fcntl.h>
31 #include <regex.h>
32 #include <stdarg.h>
33 #include <stdbool.h>
34 #include <stdio.h>
35 #include <stdlib.h>
36 #include <string.h>
37 #include <strings.h>
38 #include <sysexits.h>
39 #include <time.h>
40 #include <tls.h>
41 #include <unistd.h>
42 
43 #include "bounce.h"
44 
45 enum Cap clientCaps = CapServerTime | CapConsumer | CapPassive | CapSTS;
46 char *clientPass;
47 char *clientAway;
48 
49 static size_t active;
50 
clientAlloc(int sock,struct tls * tls)51 struct Client *clientAlloc(int sock, struct tls *tls) {
52 	struct Client *client = calloc(1, sizeof(*client));
53 	if (!client) err(EX_OSERR, "calloc");
54 	fcntl(sock, F_SETFL, O_NONBLOCK);
55 	client->sock = sock;
56 	client->tls = tls;
57 	client->time = time(NULL);
58 	client->idle = client->time;
59 	client->need = NeedHandshake | NeedNick | NeedUser;
60 	if (clientPass) client->need |= NeedPass;
61 	return client;
62 }
63 
clientHandshake(struct Client * client)64 static void clientHandshake(struct Client *client) {
65 	int error = tls_handshake(client->tls);
66 	if (error == TLS_WANT_POLLIN || error == TLS_WANT_POLLOUT) return;
67 	if (error) {
68 		warnx("client tls_handshake: %s", tls_error(client->tls));
69 		client->error = true;
70 		return;
71 	}
72 	client->need &= ~NeedHandshake;
73 	if ((clientCaps & CapSASL) && tls_peer_cert_provided(client->tls)) {
74 		client->need &= ~NeedPass;
75 	}
76 }
77 
clientFree(struct Client * client)78 void clientFree(struct Client *client) {
79 	if (!client->need) {
80 		if (!(client->caps & CapPassive) && !--active && !stateAway) {
81 			serverEnqueue("AWAY :%s\r\n", clientAway);
82 		}
83 	}
84 	tls_close(client->tls);
85 	tls_free(client->tls);
86 	free(client);
87 }
88 
clientSend(struct Client * client,const char * ptr,size_t len)89 void clientSend(struct Client *client, const char *ptr, size_t len) {
90 	verboseLog("<-", ptr, len);
91 	fcntl(client->sock, F_SETFL, 0);
92 	while (len) {
93 		ssize_t ret = tls_write(client->tls, ptr, len);
94 		if (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT) continue;
95 		if (ret < 0) {
96 			warnx("client tls_write: %s", tls_error(client->tls));
97 			client->error = true;
98 			break;
99 		}
100 		ptr += ret;
101 		len -= ret;
102 	}
103 	fcntl(client->sock, F_SETFL, O_NONBLOCK);
104 	client->idle = time(NULL);
105 }
106 
clientFormat(struct Client * client,const char * format,...)107 void clientFormat(struct Client *client, const char *format, ...) {
108 	char buf[MessageCap];
109 	va_list ap;
110 	va_start(ap, format);
111 	int len = vsnprintf(buf, sizeof(buf), format, ap);
112 	va_end(ap);
113 	assert((size_t)len < sizeof(buf));
114 	clientSend(client, buf, len);
115 }
116 
passRequired(struct Client * client)117 static void passRequired(struct Client *client) {
118 	clientFormat(
119 		client,
120 		":%s 464 * :Password incorrect\r\n"
121 		"ERROR :Password incorrect\r\n",
122 		ORIGIN
123 	);
124 	client->error = true;
125 }
126 
maybeSync(struct Client * client)127 static void maybeSync(struct Client *client) {
128 	if (client->need == NeedPass) passRequired(client);
129 	if (!client->need) {
130 		stateSync(client);
131 		if (client->setPos) ringSet(client->consumer, client->setPos);
132 		if (!(client->caps & CapPassive) && !active++) {
133 			serverEnqueue("AWAY\r\n");
134 		}
135 	}
136 }
137 
138 typedef void Handler(struct Client *client, struct Message *msg);
139 
handleNick(struct Client * client,struct Message * msg)140 static void handleNick(struct Client *client, struct Message *msg) {
141 	(void)msg;
142 	client->need &= ~NeedNick;
143 	maybeSync(client);
144 }
145 
handleUser(struct Client * client,struct Message * msg)146 static void handleUser(struct Client *client, struct Message *msg) {
147 	if (!msg->params[0]) {
148 		client->error = true;
149 		return;
150 	}
151 	if (client->need & NeedPass) {
152 		passRequired(client);
153 	} else {
154 		client->need &= ~NeedUser;
155 		client->consumer = ringConsumer(msg->params[0]);
156 		if (msg->params[0][0] == '-') client->caps |= CapPassive;
157 		maybeSync(client);
158 	}
159 }
160 
handlePass(struct Client * client,struct Message * msg)161 static void handlePass(struct Client *client, struct Message *msg) {
162 	if (!clientPass) return;
163 	if (!msg->params[0]) {
164 		client->error = true;
165 		return;
166 	}
167 #ifdef __OpenBSD__
168 	int error = crypt_checkpass(msg->params[0], clientPass);
169 #else
170 	int error = strcmp(crypt(msg->params[0], clientPass), clientPass);
171 #endif
172 	if (error) {
173 		passRequired(client);
174 	} else {
175 		client->need &= ~NeedPass;
176 		maybeSync(client);
177 	}
178 	explicit_bzero(msg->params[0], strlen(msg->params[0]));
179 }
180 
handleCap(struct Client * client,struct Message * msg)181 static void handleCap(struct Client *client, struct Message *msg) {
182 	if (!msg->params[0]) msg->params[0] = "";
183 
184 	enum Cap avail = clientCaps | (stateCaps & ~CapSASL);
185 	const char *values[CapBits] = {
186 		[CapSASLBit] = "EXTERNAL",
187 		[CapSTSBit] = "duration=2147483647",
188 	};
189 
190 	if (!strcmp(msg->params[0], "END")) {
191 		if (!client->need) return;
192 		client->need &= ~NeedCapEnd;
193 		maybeSync(client);
194 
195 	} else if (!strcmp(msg->params[0], "LS")) {
196 		if (client->need) client->need |= NeedCapEnd;
197 		int version = 0;
198 		if (msg->params[1]) version = strtol(msg->params[1], NULL, 10);
199 		if (version >= 302) {
200 			if (avail & CapCapNotify) client->caps |= CapCapNotify;
201 			clientFormat(
202 				client, ":%s CAP * LS :%s\r\n",
203 				ORIGIN, capList(avail, values)
204 			);
205 		} else {
206 			clientFormat(
207 				client, ":%s CAP * LS :%s\r\n",
208 				ORIGIN, capList(avail, NULL)
209 			);
210 		}
211 
212 	} else if (!strcmp(msg->params[0], "REQ") && msg->params[1]) {
213 		if (client->need) client->need |= NeedCapEnd;
214 		enum Cap caps = capParse(msg->params[1], values);
215 		if (caps == (avail & caps)) {
216 			client->caps |= caps;
217 			if (caps & CapConsumer && values[CapConsumerBit]) {
218 				client->setPos = strtoull(values[CapConsumerBit], NULL, 10);
219 			}
220 			clientFormat(client, ":%s CAP * ACK :%s\r\n", ORIGIN, msg->params[1]);
221 		} else {
222 			clientFormat(client, ":%s CAP * NAK :%s\r\n", ORIGIN, msg->params[1]);
223 		}
224 
225 	} else if (!strcmp(msg->params[0], "LIST")) {
226 		clientFormat(
227 			client, ":%s CAP * LIST :%s\r\n",
228 			ORIGIN, capList(client->caps, NULL)
229 		);
230 
231 	} else {
232 		clientFormat(client, ":%s 410 * :Invalid CAP subcommand\r\n", ORIGIN);
233 	}
234 }
235 
handleAuthenticate(struct Client * client,struct Message * msg)236 static void handleAuthenticate(struct Client *client, struct Message *msg) {
237 	if (!msg->params[0]) msg->params[0] = "";
238 	bool cert = (clientCaps & CapSASL) && tls_peer_cert_provided(client->tls);
239 	if (cert && !strcmp(msg->params[0], "EXTERNAL")) {
240 		clientFormat(client, "AUTHENTICATE +\r\n");
241 	} else if (cert && !strcmp(msg->params[0], "+")) {
242 		clientFormat(
243 			client, ":%s 900 * %s * :You are now logged in as *\r\n",
244 			ORIGIN, stateEcho()
245 		);
246 		clientFormat(
247 			client, ":%s 903 * :SASL authentication successful\r\n",
248 			ORIGIN
249 		);
250 	} else {
251 		clientFormat(
252 			client, ":%s 904 * :SASL authentication failed\r\n",
253 			ORIGIN
254 		);
255 	}
256 }
257 
handleQuit(struct Client * client,struct Message * msg)258 static void handleQuit(struct Client *client, struct Message *msg) {
259 	(void)msg;
260 	clientFormat(client, "ERROR :Detaching\r\n");
261 	client->error = true;
262 }
263 
hasTag(const char * tags,const char * tag)264 static bool hasTag(const char *tags, const char *tag) {
265 	if (!tags) return false;
266 	size_t len = strlen(tag);
267 	bool val = strchr(tag, '=');
268 	while (*tags && *tags != ' ') {
269 		if (
270 			!strncmp(tags, tag, len) &&
271 			(!tags[len] || strchr((val ? "; " : "=; "), tags[len]))
272 		) return true;
273 		tags += strcspn(tags, "; ");
274 		tags += (*tags == ';');
275 	}
276 	return false;
277 }
278 
synthLabel(struct Client * client)279 static const char *synthLabel(struct Client *client) {
280 	enum { LabelCap = 64 };
281 	static char buf[sizeof("label=") + LabelCap];
282 	snprintf(buf, sizeof(buf), "label=pounce~%zu", client->consumer);
283 	return buf;
284 }
285 
reserialize(char * buf,size_t cap,const char * origin,const struct Message * msg)286 static void reserialize(
287 	char *buf, size_t cap, const char *origin, const struct Message *msg
288 ) {
289 	char *ptr = buf, *end = &buf[cap];
290 	if (msg->tags) {
291 		ptr = seprintf(ptr, end, "@%s ", msg->tags);
292 	}
293 	if (origin || msg->origin) {
294 		ptr = seprintf(ptr, end, ":%s ", (origin ? origin : msg->origin));
295 	}
296 	ptr = seprintf(ptr, end, "%s", msg->cmd);
297 	for (size_t i = 0; i < ParamCap && msg->params[i]; ++i) {
298 		if (i + 1 == ParamCap || !msg->params[i + 1]) {
299 			ptr = seprintf(ptr, end, " :%s", msg->params[i]);
300 		} else {
301 			ptr = seprintf(ptr, end, " %s", msg->params[i]);
302 		}
303 	}
304 }
305 
clientProduce(struct Client * client,const char * line)306 static void clientProduce(struct Client *client, const char *line) {
307 	size_t diff = ringDiff(client->consumer);
308 	ringProduce(line);
309 	if (!diff && !(client->caps & CapEchoMessage)) {
310 		ringConsume(NULL, client->consumer);
311 	}
312 }
313 
handlePrivmsg(struct Client * client,struct Message * msg)314 static void handlePrivmsg(struct Client *client, struct Message *msg) {
315 	if (!msg->params[0]) return;
316 	char buf[MessageCap];
317 	bool self = !strcmp(msg->params[0], stateNick());
318 	if (!(stateCaps & CapEchoMessage) || self) {
319 		reserialize(buf, sizeof(buf), stateEcho(), msg);
320 		clientProduce(client, buf);
321 	}
322 	if (self) return;
323 	reserialize(buf, sizeof(buf), NULL, msg);
324 	if (stateCaps & CapEchoMessage && !hasTag(msg->tags, "label")) {
325 		serverFormat(
326 			"@%s%c%s\r\n",
327 			synthLabel(client),
328 			(buf[0] == '@' ? ';' : ' '),
329 			(buf[0] == '@' ? &buf[1] : buf)
330 		);
331 	} else {
332 		serverFormat("%s\r\n", buf);
333 	}
334 }
335 
handlePalaver(struct Client * client,struct Message * msg)336 static void handlePalaver(struct Client *client, struct Message *msg) {
337 	if (client->need & NeedPass) return;
338 	char buf[MessageCap];
339 	reserialize(buf, sizeof(buf), NULL, msg);
340 	clientProduce(client, buf);
341 }
342 
handlePong(struct Client * client,struct Message * msg)343 static void handlePong(struct Client *client, struct Message *msg) {
344 	(void)client;
345 	(void)msg;
346 }
347 
348 static const struct {
349 	bool intercept;
350 	bool need;
351 	const char *cmd;
352 	Handler *fn;
353 } Handlers[] = {
354 	{ false, false, "AUTHENTICATE", handleAuthenticate },
355 	{ false, false, "NICK", handleNick },
356 	{ false, false, "PASS", handlePass },
357 	{ false, false, "USER", handleUser },
358 	{ true, false, "CAP", handleCap },
359 	{ true, false, "PALAVER", handlePalaver },
360 	{ true, false, "PONG", handlePong },
361 	{ true, true, "NOTICE", handlePrivmsg },
362 	{ true, true, "PRIVMSG", handlePrivmsg },
363 	{ true, true, "QUIT", handleQuit },
364 	{ true, true, "TAGMSG", handlePrivmsg },
365 };
366 
clientParse(struct Client * client,char * line)367 static void clientParse(struct Client *client, char *line) {
368 	struct Message msg = parse(line);
369 	if (!msg.cmd) return;
370 	for (size_t i = 0; i < ARRAY_LEN(Handlers); ++i) {
371 		if (strcmp(msg.cmd, Handlers[i].cmd)) continue;
372 		if (Handlers[i].need && client->need) break;
373 		Handlers[i].fn(client, &msg);
374 		return;
375 	}
376 	client->error = true;
377 }
378 
intercept(const char * line,size_t len)379 static bool intercept(const char *line, size_t len) {
380 	if (line[0] == '@') {
381 		const char *sp = memchr(line, ' ', len);
382 		if (!sp) return false;
383 		sp++;
384 		len -= sp - line;
385 		line = sp;
386 	}
387 	for (size_t i = 0; i < ARRAY_LEN(Handlers); ++i) {
388 		if (!Handlers[i].intercept) continue;
389 		size_t n = strlen(Handlers[i].cmd);
390 		if (len < n) continue;
391 		if (memcmp(line, Handlers[i].cmd, n)) continue;
392 		if (len == n || line[n] == ' ' || line[n] == '\r') return true;
393 	}
394 	return false;
395 }
396 
clientRecv(struct Client * client)397 void clientRecv(struct Client *client) {
398 	if (client->need & NeedHandshake) {
399 		clientHandshake(client);
400 		return;
401 	}
402 
403 	ssize_t read = tls_read(
404 		client->tls,
405 		&client->buf[client->len], sizeof(client->buf) - client->len
406 	);
407 	if (read == TLS_WANT_POLLIN || read == TLS_WANT_POLLOUT) return;
408 	if (read <= 0) {
409 		if (read < 0) warnx("client tls_read: %s", tls_error(client->tls));
410 		client->error = true;
411 		return;
412 	}
413 	client->len += read;
414 
415 	char *lf;
416 	char *line = client->buf;
417 	for (;;) {
418 		lf = memchr(line, '\n', &client->buf[client->len] - line);
419 		if (!lf) break;
420 		verboseLog("->", line, lf - line);
421 		if (client->need || intercept(line, lf - line)) {
422 			lf[0] = '\0';
423 			if (lf - line && lf[-1] == '\r') lf[-1] = '\0';
424 			clientParse(client, line);
425 		} else {
426 			serverSend(line, lf + 1 - line);
427 		}
428 		line = lf + 1;
429 	}
430 	client->len -= line - client->buf;
431 	memmove(client->buf, line, client->len);
432 	client->idle = time(NULL);
433 }
434 
wordcmp(const char * line,size_t i,const char * word)435 static int wordcmp(const char *line, size_t i, const char *word) {
436 	if (line[0] == '@') {
437 		line += strcspn(line, " ");
438 		if (*line) line++;
439 	}
440 	if (line[0] == ':') {
441 		line += strcspn(line, " ");
442 		if (*line) line++;
443 	}
444 	while (i--) {
445 		line += strcspn(line, " ");
446 		if (*line) line++;
447 	}
448 	size_t len = strcspn(line, " ");
449 	return len == strlen(word)
450 		? strncmp(line, word, len)
451 		: (int)len - (int)strlen(word);
452 }
453 
454 // s/..(..)../\1/g
455 static char *
snip(char * dst,size_t cap,const char * src,const regex_t * regex)456 snip(char *dst, size_t cap, const char *src, const regex_t *regex) {
457 	char *ptr = dst, *end = &dst[cap];
458 	regmatch_t match[2];
459 	assert(regex->re_nsub);
460 	for (; *src; src += match[0].rm_eo) {
461 		if (regexec(regex, src, 2, match, 0)) break;
462 		ptr = seprintf(
463 			ptr, end, "%.*s%.*s",
464 			(int)match[0].rm_so, src,
465 			(int)(match[1].rm_eo - match[1].rm_so), &src[match[1].rm_so]
466 		);
467 	}
468 	ptr = seprintf(ptr, end, "%s", src);
469 	return (ptr == end ? NULL : dst);
470 }
471 
compile(regex_t * regex,const char * pattern)472 static regex_t *compile(regex_t *regex, const char *pattern) {
473 	if (regex->re_nsub) return regex;
474 	int error = regcomp(regex, pattern, REG_EXTENDED);
475 	if (error) {
476 		char buf[256];
477 		regerror(error, regex, buf, sizeof(buf));
478 		errx(EX_SOFTWARE, "regcomp: %s: %s", buf, pattern);
479 	}
480 	return regex;
481 }
482 
483 typedef const char *Filter(const char *line);
484 
filterAccountNotify(const char * line)485 static const char *filterAccountNotify(const char *line) {
486 	return (wordcmp(line, 0, "ACCOUNT") ? line : NULL);
487 }
488 
filterAwayNotify(const char * line)489 static const char *filterAwayNotify(const char *line) {
490 	return (wordcmp(line, 0, "AWAY") ? line : NULL);
491 }
492 
filterBatch(const char * line)493 static const char *filterBatch(const char *line) {
494 	return (wordcmp(line, 0, "BATCH") ? line : NULL);
495 }
496 
filterCapNotify(const char * line)497 static const char *filterCapNotify(const char *line) {
498 	if (wordcmp(line, 0, "CAP")) return line;
499 	if (!wordcmp(line, 1, "NEW")) return NULL;
500 	if (!wordcmp(line, 1, "DEL")) return NULL;
501 	return line;
502 }
503 
filterChghost(const char * line)504 static const char *filterChghost(const char *line) {
505 	return (wordcmp(line, 0, "CHGHOST") ? line : NULL);
506 }
507 
filterExtendedJoin(const char * line)508 static const char *filterExtendedJoin(const char *line) {
509 	if (wordcmp(line, 0, "JOIN")) return line;
510 	static regex_t regex;
511 	static char buf[MessageCap];
512 	return snip(buf, sizeof(buf), line, compile(&regex, "(JOIN [^ ]+).+"));
513 }
514 
filterInviteNotify(const char * line)515 static const char *filterInviteNotify(const char *line) {
516 	if (wordcmp(line, 0, "INVITE")) return line;
517 	return (wordcmp(line, 1, stateNick()) ? NULL : line);
518 }
519 
filterLabeledResponse(const char * line)520 static const char *filterLabeledResponse(const char *line) {
521 	return (wordcmp(line, 0, "ACK") ? line : NULL);
522 }
523 
filterMessageTags(const char * line)524 static const char *filterMessageTags(const char *line) {
525 	return (wordcmp(line, 0, "TAGMSG") ? line : NULL);
526 }
527 
filterMultiPrefix(const char * line)528 static const char *filterMultiPrefix(const char *line) {
529 	static char buf[MessageCap];
530 	if (!wordcmp(line, 0, "352")) {
531 		static regex_t regex;
532 		return snip(
533 			buf, sizeof(buf), line,
534 			compile(&regex, "( [HG][*]?[~!@%&+])[~!@%&+]+")
535 		);
536 	} else if (!wordcmp(line, 0, "353")) {
537 		static regex_t regex;
538 		return snip(
539 			buf, sizeof(buf), line,
540 			compile(&regex, "( :?[~!@%&+])[~!@%&+]+")
541 		);
542 	} else {
543 		return line;
544 	}
545 }
546 
filterPalaverApp(const char * line)547 static const char *filterPalaverApp(const char *line) {
548 	return (wordcmp(line, 0, "PALAVER") ? line : NULL);
549 }
550 
filterSetname(const char * line)551 static const char *filterSetname(const char *line) {
552 	return (wordcmp(line, 0, "SETNAME") ? line : NULL);
553 }
554 
filterUserhostInNames(const char * line)555 static const char *filterUserhostInNames(const char *line) {
556 	if (wordcmp(line, 0, "353")) return line;
557 	static regex_t regex;
558 	static char buf[MessageCap];
559 	return snip(
560 		buf, sizeof(buf), line,
561 		compile(&regex, "( :?[^!]+)![^ ]+")
562 	);
563 }
564 
565 static Filter *Filters[CapBits] = {
566 	[CapAccountNotifyBit] = filterAccountNotify,
567 	[CapAwayNotifyBit] = filterAwayNotify,
568 	[CapBatchBit] = filterBatch,
569 	[CapCapNotifyBit] = filterCapNotify,
570 	[CapChghostBit] = filterChghost,
571 	[CapExtendedJoinBit] = filterExtendedJoin,
572 	[CapInviteNotifyBit] = filterInviteNotify,
573 	[CapLabeledResponseBit] = filterLabeledResponse,
574 	[CapMessageTagsBit] = filterMessageTags,
575 	[CapMultiPrefixBit] = filterMultiPrefix,
576 	[CapPalaverAppBit] = filterPalaverApp,
577 	[CapSetnameBit] = filterSetname,
578 	[CapUserhostInNamesBit] = filterUserhostInNames,
579 };
580 
filterEchoMessage(struct Client * client,const char * line)581 static const char *filterEchoMessage(struct Client *client, const char *line) {
582 	if (line[0] != '@') return line;
583 	if (!hasTag(&line[1], synthLabel(client))) return line;
584 	return NULL;
585 }
586 
filterTags(const char * line)587 static const char *filterTags(const char *line) {
588 	if (line[0] != '@') return line;
589 	const char *sp = strchr(line, ' ');
590 	return (sp ? sp + 1 : NULL);
591 }
592 
clientConsume(struct Client * client)593 void clientConsume(struct Client *client) {
594 	struct timeval time;
595 	const char *line = ringPeek(&time, client->consumer);
596 	if (!line) return;
597 
598 	enum Cap diff = client->caps ^ (clientCaps | stateCaps);
599 	if (diff & CapEchoMessage) {
600 		line = filterEchoMessage(client, line);
601 	}
602 	if (line && stateCaps & TagCaps && !(client->caps & TagCaps)) {
603 		line = filterTags(line);
604 	}
605 	for (size_t i = 0; line && i < ARRAY_LEN(Filters); ++i) {
606 		if (!Filters[i]) continue;
607 		if (diff & (1 << i)) line = Filters[i](line);
608 	}
609 	if (!line) {
610 		ringConsume(NULL, client->consumer);
611 		return;
612 	}
613 
614 	if (
615 		client->caps & CapServerTime &&
616 		(line[0] != '@' || !hasTag(&line[1], "time"))
617 	) {
618 		char ts[sizeof("YYYY-MM-DDThh:mm:ss")];
619 		struct tm *tm = gmtime(&time.tv_sec);
620 		strftime(ts, sizeof(ts), "%FT%T", tm);
621 		clientFormat(
622 			client, "@time=%s.%03dZ;causal.agency/pos=%zu%c%s\r\n",
623 			ts, (int)(time.tv_usec / 1000),
624 			ringPos(client->consumer) + 1,
625 			(line[0] == '@' ? ';' : ' '),
626 			(line[0] == '@' ? &line[1] : line)
627 		);
628 	} else if (client->caps & CapConsumer) {
629 		clientFormat(
630 			client, "@causal.agency/pos=%zu%c%s\r\n",
631 			ringPos(client->consumer) + 1,
632 			(line[0] == '@' ? ';' : ' '),
633 			(line[0] == '@' ? &line[1] : line)
634 		);
635 	} else {
636 		clientFormat(client, "%s\r\n", line);
637 	}
638 	if (!client->error) ringConsume(NULL, client->consumer);
639 }
640