]> jfr.im git - irc/quakenet/newserv.git/blob - nterfacer/nterfacer.c
Add malloc checking to lua/nterfacer/trojanscan.
[irc/quakenet/newserv.git] / nterfacer / nterfacer.c
1 /*
2 nterfacer
3 Copyright (C) 2004-2007 Chris Porter.
4 */
5
6 #include <stdio.h>
7 #include <stdlib.h>
8 #include <stdarg.h>
9 #include <sys/poll.h>
10 #include <sys/types.h>
11 #include <sys/socket.h>
12 #include <errno.h>
13 #include <unistd.h>
14 #include <sys/ioctl.h>
15 #include <netdb.h>
16 #include <string.h>
17 #include <strings.h>
18
19 #include "../lib/sstring.h"
20 #include "../lib/irc_string.h"
21 #include "../core/config.h"
22 #include "../core/events.h"
23 #include "../lib/version.h"
24
25 #include "nterfacer.h"
26 #include "logging.h"
27
28 MODULE_VERSION("1.1p" PROTOCOL_VERSION);
29
30 struct service_node *tree = NULL;
31 struct esocket_events nterfacer_events;
32 struct esocket *nterfacer_sock;
33 struct rline *rlines = NULL;
34 unsigned short nterfacer_token = BLANK_TOKEN;
35 struct nterface_auto_log *nrl;
36
37 struct service_node *ping = NULL;
38 int accept_fd = -1;
39 struct permitted *permits;
40 int permit_count = 0;
41
42 int ping_handler(struct rline *ri, int argc, char **argv);
43
44 void _init(void) {
45 int loaded;
46 int debug_mode = getcopyconfigitemintpositive("nterfacer", "debug", 0);
47
48 nrl = nterface_open_log("nterfacer", "logs/nterfacer.log", debug_mode);
49
50 loaded = load_permits();
51 if(!loaded) {
52 nterface_log(nrl, NL_ERROR, "No permits loaded successfully.");
53 return;
54 } else {
55 nterface_log(nrl, NL_INFO, "Loaded %d permit%s successfully.", loaded, loaded==1?"":"s");
56 }
57
58 nterfacer_events.on_accept = nterfacer_accept_event;
59 nterfacer_events.on_line = nterfacer_line_event;
60 nterfacer_events.on_disconnect = NULL;
61
62 nterfacer_token = esocket_token();
63
64 ping = register_service("nterfacer");
65 if(!ping) {
66 MemError();
67 } else {
68 register_handler(ping, "ping", 0, ping_handler);
69 }
70
71 accept_fd = setup_listening_socket();
72 if(accept_fd == -1) {
73 nterface_log(nrl, NL_ERROR, "Unable to setup listening socket!");
74 } else {
75 nterfacer_sock = esocket_add(accept_fd, ESOCKET_UNIX_DOMAIN, &nterfacer_events, nterfacer_token);
76 }
77
78 /* the main unix domain socket must NOT have a disconnect event. */
79 nterfacer_events.on_disconnect = nterfacer_disconnect_event;
80 }
81
82 void free_handler(struct handler *hp) {
83 freesstring(hp->command);
84 ntfree(hp);
85 }
86
87 void free_handlers(struct service_node *tp) {
88 struct handler *hp, *lp;
89
90 for(hp=tp->handlers;hp;) {
91 lp = hp;
92 hp = hp->next;
93 free_handler(lp);
94 }
95
96 tp->handlers = NULL;
97 }
98
99 void _fini(void) {
100 struct service_node *tp, *lp;
101 int i;
102
103 if(ping)
104 deregister_service(ping);
105
106 for(tp=tree;tp;) {
107 lp = tp;
108 tp = tp->next;
109 free_handlers(lp);
110 ntfree(lp);
111 }
112 tree = NULL;
113
114 if((accept_fd != -1) && nterfacer_sock) {
115 esocket_clean_by_token(nterfacer_token);
116 nterfacer_sock = NULL;
117 accept_fd = -1;
118 }
119
120 if(permits && permit_count) {
121 for(i=0;i<permit_count;i++) {
122 freesstring(permits[i].hostname);
123 freesstring(permits[i].password);
124 }
125 ntfree(permits);
126 permit_count = 0;
127 permits = NULL;
128 }
129
130 nrl = nterface_close_log(nrl);
131 nscheckfreeall(POOL_NTERFACER);
132 }
133
134 int load_permits(void) {
135 int loaded_lines = 0, i, j;
136 struct permitted *new_permits, *resized, *item;
137 struct hostent *host;
138 array *hostnamesa, *passwordsa;
139 sstring **hostnames, **passwords;
140
141 hostnamesa = getconfigitems("nterfacer", "hostname");
142 passwordsa = getconfigitems("nterfacer", "password");
143 if(!hostnamesa || !passwordsa) {
144 nterface_log(nrl, NL_ERROR, "Unable to load hostnames/passwords.");
145 return 0;
146 }
147 if(hostnamesa->cursi != passwordsa->cursi) {
148 nterface_log(nrl, NL_ERROR, "Different number of hostnames/passwords in config file.");
149 return 0;
150 }
151
152 hostnames = (sstring **)hostnamesa->content;
153 passwords = (sstring **)passwordsa->content;
154
155 new_permits = ntmalloc(hostnamesa->cursi * sizeof(struct permitted));
156 memset(new_permits, 0, hostnamesa->cursi * sizeof(struct permitted));
157 item = new_permits;
158
159 for(i=0;i<hostnamesa->cursi;i++) {
160 item->hostname = getsstring(hostnames[i]->content, hostnames[i]->length);
161
162 host = gethostbyname(item->hostname->content);
163 if (!host) {
164 nterface_log(nrl, NL_WARNING, "Couldn't resolve hostname: %s (item %d).", item->hostname->content, i + 1);
165 freesstring(item->hostname);
166 continue;
167 }
168
169 item->ihost = (*(struct in_addr *)host->h_addr).s_addr;
170 for(j=0;j<loaded_lines;j++) {
171 if(new_permits[j].ihost == item->ihost) {
172 nterface_log(nrl, NL_WARNING, "Host with items %d and %d is identical, dropping item %d.", j + 1, i + 1, i + 1);
173 host = NULL;
174 }
175 }
176
177 if(!host) {
178 freesstring(item->hostname);
179 continue;
180 }
181
182 item->password = getsstring(passwords[i]->content, passwords[i]->length);
183 nterface_log(nrl, NL_DEBUG, "Loaded permit, hostname: %s.", item->hostname->content);
184
185 item++;
186 loaded_lines++;
187 }
188
189 if(!loaded_lines) {
190 ntfree(new_permits);
191 return 0;
192 }
193
194 resized = ntrealloc(new_permits, sizeof(struct permitted) * loaded_lines);
195 if(!resized) {
196 MemError();
197 ntfree(new_permits);
198 return 0;
199 }
200 permits = resized;
201 permit_count = loaded_lines;
202
203 return permit_count;
204 }
205
206 int setup_listening_socket(void) {
207 struct sockaddr_in sin;
208 int fd;
209 unsigned int opt = 1;
210
211 fd = socket(AF_INET, SOCK_STREAM, 0);
212
213 /* also shamelessly ripped from proxyscan */
214 if(fd == -1) {
215 nterface_log(nrl, NL_ERROR, "Unable to open listening socket (%d).", errno);
216 return -1;
217 }
218
219 if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (const char *) &opt, sizeof(opt)) != 0) {
220 nterface_log(nrl, NL_ERROR, "Unable to set SO_REUSEADDR on listen socket.");
221 return -1;
222 }
223
224 /* Initialiase the addresses */
225 memset(&sin, 0, sizeof(sin));
226 sin.sin_family = AF_INET;
227 sin.sin_port = htons(getcopyconfigitemintpositive("nterfacer", "port", NTERFACER_PORT));
228
229 if(bind(fd, (struct sockaddr *) &sin, sizeof(sin))) {
230 nterface_log(nrl, NL_ERROR, "Unable to bind listen socket (%d).", errno);
231 return -1;
232 }
233
234 listen(fd, 5);
235
236 if(ioctl(fd, FIONBIO, &opt)) {
237 nterface_log(nrl, NL_ERROR, "Unable to set listen socket non-blocking.");
238 return -1;
239 }
240
241 return fd;
242 }
243
244 struct service_node *register_service(char *name) {
245 struct service_node *np = ntmalloc(sizeof(service_node));
246 MemCheckR(np, NULL);
247
248 np->name = getsstring(name, strlen(name));
249 if(!np->name) {
250 MemError();
251 ntfree(np);
252 return NULL;
253 }
254
255 np->handlers = NULL;
256 np->next = tree;
257 tree = np;
258
259 return np;
260 }
261
262 struct handler *register_handler(struct service_node *service, char *command, int args, handler_function fp) {
263 struct handler *hp = ntmalloc(sizeof(handler));
264 MemCheckR(hp, NULL);
265
266 hp->command = getsstring(command, strlen(command));
267 if(!hp->command) {
268 MemError();
269 ntfree(hp);
270 return NULL;
271 }
272
273 hp->function = fp;
274 hp->args = args;
275
276 hp->next = service->handlers;
277 hp->service = service;
278 service->handlers = hp;
279
280 return hp;
281 }
282
283 void deregister_handler(struct handler *hl) {
284 struct service_node *service = (struct service_node *)hl->service;
285 struct handler *np, *lp = NULL;
286 for(np=service->handlers;np;lp=np,np=np->next) {
287 if(hl == np) {
288 if(lp) {
289 lp->next = np->next;
290 } else {
291 service->handlers = np->next;
292 }
293 free_handler(np);
294 return;
295 }
296 }
297 }
298
299 void deregister_service(struct service_node *service) {
300 struct service_node *sp, *lp = NULL;
301 struct rline *li, *pi = NULL;
302
303 for(sp=tree;sp;lp=sp,sp=sp->next) {
304 if(sp == service) {
305 if(lp) {
306 lp->next = sp->next;
307 } else {
308 tree = sp->next;
309 }
310 break;
311 }
312 }
313
314 if(!sp) /* already freed */
315 return;
316
317 free_handlers(service);
318
319 for(li=rlines;li;) {
320 if(li->service == service) {
321 if(pi) {
322 pi->next = li->next;
323 ntfree(li);
324 li = pi->next;
325 } else {
326 rlines = li->next;
327 ntfree(li);
328 li = rlines;
329 }
330 } else {
331 pi=li,li=li->next;
332 }
333 }
334 freesstring(service->name);
335
336 ntfree(service);
337 }
338
339 void nterfacer_accept_event(struct esocket *socket) {
340 struct sockaddr_in sin;
341 unsigned int addrsize = sizeof(sin);
342 int newfd = accept(socket->fd, (struct sockaddr *)&sin, &addrsize), i;
343 struct sconnect *temp;
344 struct permitted *item = NULL;
345 struct esocket *newsocket;
346 unsigned int opt = 1;
347
348 if(newfd == -1) {
349 nterface_log(nrl, NL_WARNING, "Unable to accept nterfacer fd!");
350 return;
351 }
352
353 if(ioctl(newfd, FIONBIO, &opt)) {
354 nterface_log(nrl, NL_ERROR, "Unable to set accepted socket non-blocking.");
355 return;
356 }
357
358 for(i=0;i<permit_count;i++) {
359 if(permits[i].ihost == sin.sin_addr.s_addr) {
360 item = &permits[i];
361 break;
362 }
363 }
364
365 if(!item) {
366 /* Someone needs to figure out how to print the IP :) */
367 nterface_log(nrl, NL_INFO, "Unauthorised connection closed");
368 close(newfd);
369 return;
370 }
371
372 temp = (struct sconnect *)ntmalloc(sizeof(struct sconnect));
373 if(!temp) {
374 MemError();
375 close(newfd);
376 return;
377 }
378
379 /* do checks on hostname first */
380
381 newsocket = esocket_add(newfd, ESOCKET_UNIX_DOMAIN_CONNECTED, &nterfacer_events, nterfacer_token);
382 if(!newsocket) {
383 ntfree(temp);
384 close(newfd);
385 return;
386 }
387 newsocket->tag = temp;
388
389 nterface_log(nrl, NL_INFO, "New connection from %s.", item->hostname->content);
390
391 temp->status = SS_IDLE;
392 temp->permit = item;
393
394 esocket_write_line(newsocket, "nterfacer " PROTOCOL_VERSION);
395 }
396
397 void derive_key(unsigned char *out, char *password, char *segment, unsigned char *noncea, unsigned char *nonceb, unsigned char *extra, int extralen) {
398 SHA256_CTX c;
399 SHA256_Init(&c);
400 SHA256_Update(&c, (unsigned char *)password, strlen(password));
401 SHA256_Update(&c, (unsigned char *)":", 1);
402 SHA256_Update(&c, (unsigned char *)segment, strlen(segment));
403 SHA256_Update(&c, (unsigned char *)":", 1);
404 SHA256_Update(&c, noncea, 16);
405 SHA256_Update(&c, (unsigned char *)":", 1);
406 SHA256_Update(&c, nonceb, 16);
407 SHA256_Update(&c, (unsigned char *)":", 1);
408 SHA256_Update(&c, extra, extralen);
409 SHA256_Final(out, &c);
410
411 SHA256_Init(&c);
412 SHA256_Update(&c, out, 32);
413 SHA256_Final(out, &c);
414 }
415
416 int nterfacer_line_event(struct esocket *sock, char *newline) {
417 struct sconnect *socket = sock->tag;
418 char *response, *theirnonceh = NULL, *theirivh = NULL;
419 unsigned char theirnonce[16], theiriv[16];
420 int number, reason;
421
422 switch(socket->status) {
423 case SS_IDLE:
424 if(strcasecmp(newline, ANTI_FULL_VERSION)) {
425 nterface_log(nrl, NL_INFO, "Protocol mismatch from %s: %s", socket->permit->hostname->content, newline);
426 return 1;
427 } else {
428 unsigned char challenge[32];
429 char ivhex[16 * 2 + 1], noncehex[16 * 2 + 1];
430
431 if(!get_entropy(challenge, 32) || !get_entropy(socket->iv, 16)) {
432 nterface_log(nrl, NL_ERROR, "Unable to open challenge/IV entropy bin!");
433 return 1;
434 }
435
436 int_to_hex(challenge, socket->challenge, 32);
437 int_to_hex(socket->iv, ivhex, 16);
438
439 memcpy(socket->response, challenge_response(socket->challenge, socket->permit->password->content), sizeof(socket->response));
440 socket->response[sizeof(socket->response) - 1] = '\0'; /* just in case */
441
442 socket->status = SS_VERSIONED;
443 if(!generate_nonce(socket->ournonce, 1)) {
444 nterface_log(nrl, NL_ERROR, "Unable to generate nonce!");
445 return 1;
446 }
447 int_to_hex(socket->ournonce, noncehex, 16);
448
449 if(esocket_write_line(sock, "%s %s %s", socket->challenge, ivhex, noncehex))
450 return BUF_ERROR;
451 return 0;
452 }
453 break;
454 case SS_VERSIONED:
455 for(response=newline;*response;response++) {
456 if((*response == ' ') && (*(response + 1))) {
457 *response = '\0';
458 theirivh = response + 1;
459 break;
460 }
461 }
462
463 if(theirivh) {
464 for(response=theirivh;*response;response++) {
465 if((*response == ' ') && (*(response + 1))) {
466 *response = '\0';
467 theirnonceh = response + 1;
468 break;
469 }
470 }
471 }
472
473 if(!theirivh || (strlen(theirivh) != 32) || !hex_to_int(theirivh, theiriv, sizeof(theiriv)) ||
474 !theirnonceh || (strlen(theirnonceh) != 32) || !hex_to_int(theirnonceh, theirnonce, sizeof(theirnonce))) {
475 nterface_log(nrl, NL_INFO, "Protocol error drop: %s", socket->permit->hostname->content);
476 return 1;
477 }
478
479 if(!memcmp(socket->ournonce, theirnonce, sizeof(theirnonce))) {
480 nterface_log(nrl, NL_INFO, "Bad nonce drop: %s", socket->permit->hostname->content);
481 return 1;
482 }
483
484 if(!strncasecmp(newline, socket->response, sizeof(socket->response))) {
485 unsigned char theirkey[32], ourkey[32];
486
487 derive_key(ourkey, socket->permit->password->content, socket->challenge, socket->ournonce, theirnonce, (unsigned char *)"SERVER", 6);
488
489 derive_key(theirkey, socket->permit->password->content, socket->response, theirnonce, socket->ournonce, (unsigned char *)"CLIENT", 6);
490 nterface_log(nrl, NL_INFO, "Authed: %s", socket->permit->hostname->content);
491 socket->status = SS_AUTHENTICATED;
492 switch_buffer_mode(sock, ourkey, socket->iv, theirkey, theiriv);
493
494 if(esocket_write_line(sock, "Oauth"))
495 return BUF_ERROR;
496 } else {
497 nterface_log(nrl, NL_INFO, "Bad CR drop: %s", socket->permit->hostname->content);
498
499 return 1;
500 }
501 break;
502 case SS_AUTHENTICATED:
503 nterface_log(nrl, NL_INFO|NL_LOG_ONLY, "L(%s): %s", socket->permit->hostname->content, newline);
504 reason = nterfacer_new_rline(newline, sock, &number);
505 if(reason) {
506 if(reason == RE_SOCKET_ERROR)
507 return BUF_ERROR;
508 if(reason != RE_BAD_LINE) {
509 if(esocket_write_line(sock, "%d,E%d,%s", number, reason, request_error(reason)))
510 return BUF_ERROR;
511 return 0;
512 } else {
513 return 1;
514 }
515 }
516 break;
517 }
518
519 return 0;
520 }
521
522 int nterfacer_new_rline(char *line, struct esocket *socket, int *number) {
523 char *sp, *p, *parsebuf = NULL, *pp, commandbuf[MAX_BUFSIZE], *args[MAX_ARGS], *newp;
524 int argcount;
525 struct service_node *service;
526 struct rline *prequest;
527 struct handler *hl;
528 int re;
529
530 if(!line || !line[0] || (line[0] == ','))
531 return 0;
532
533 for(sp=line;*sp;sp++)
534 if(*sp == ',')
535 break;
536
537 if(!*sp || !*(sp + 1))
538 return RE_BAD_LINE;
539
540 *sp = '\0';
541
542 for(service=tree;service;service=service->next)
543 if(!strcmp(service->name->content, line))
544 break;
545
546 for(p=sp+1;*p;p++)
547 if(*p == ',')
548 break;
549
550 if(!*p || !(p + 1))
551 return RE_BAD_LINE;
552
553 *p = '\0';
554 *number = positive_atoi(sp + 1);
555
556 if((*number < 1) || (*number > 0xffff))
557 return RE_BAD_LINE;
558
559 if (!service) {
560 nterface_log(nrl, NL_DEBUG, "Unable to find service: %s", line);
561 return RE_SERVICER_NOT_FOUND;
562 }
563
564 newp = commandbuf;
565
566 for(pp=p+1;*pp;pp++) {
567 if((*pp == '\\') && *(pp + 1)) {
568 if(*(pp + 1) == ',') {
569 *newp++ = ',';
570 } else if(*(pp + 1) == '\\') {
571 *newp++ = '\\';
572 }
573 pp++;
574 } else if(*pp == ',') {
575 break;
576 } else {
577 *newp++ = *pp;
578 }
579 }
580
581 if(*pp == '\0') { /* if we're at the end already, we have no arguments */
582 argcount = 0;
583 } else {
584 argcount = 1; /* we have a comma, so we have one already */
585 }
586
587 *newp = '\0';
588
589 for(hl=service->handlers;hl;hl=hl->next)
590 if(!strncmp(hl->command->content, commandbuf, sizeof(commandbuf)))
591 break;
592
593 if(!hl)
594 return RE_COMMAND_NOT_FOUND;
595
596 if(argcount) {
597 parsebuf = (char *)ntmalloc(strlen(pp) + 1);
598 MemCheckR(parsebuf, RE_MEM_ERROR);
599 newp = parsebuf;
600
601 for(newp=args[0]=parsebuf,pp++;*pp;pp++) {
602 if((*pp == '\\') && *(pp + 1)) {
603 if(*(pp + 1) == ',') {
604 *newp++ = ',';
605 } else if(*(pp + 1) == '\\') {
606 *newp++ = '\\';
607 }
608 pp++;
609 } else if(*pp == ',') {
610 *newp++ = '\0';
611 args[argcount++] = newp;
612 if(argcount > MAX_ARGS) {
613 ntfree(parsebuf);
614 return RE_TOO_MANY_ARGS;
615 }
616 } else {
617 *newp++ = *pp;
618 }
619 }
620 *newp = '\0';
621 }
622 if(argcount < hl->args) {
623 if(argcount && parsebuf)
624 ntfree(parsebuf);
625 return RE_WRONG_ARG_COUNT;
626 }
627
628 prequest = (struct rline *)ntmalloc(sizeof(struct rline));
629 if(!prequest) {
630 MemError();
631 if(argcount && parsebuf)
632 ntfree(parsebuf);
633 return RE_MEM_ERROR;
634 }
635
636 prequest->service = service;
637 prequest->handler = hl;
638 prequest->buf[0] = '\0';
639 prequest->curpos = prequest->buf;
640 prequest->tag = NULL;
641 prequest->id = *number;
642 prequest->next = rlines;
643 prequest->socket = socket;
644
645 rlines = prequest;
646 re = (hl->function)(prequest, argcount, args);
647
648 if(argcount && parsebuf)
649 ntfree(parsebuf);
650
651 return re;
652 }
653
654 void nterfacer_disconnect_event(struct esocket *sock) {
655 struct sconnect *socket = sock->tag;
656 struct rline *li;
657 /* not tested */
658
659 nterface_log(nrl, NL_INFO, "Disconnected from %s.", socket->permit->hostname->content);
660
661 /* not tested */
662 for(li=rlines;li;li=li->next)
663 if(li->socket->tag == socket)
664 li->socket = NULL;
665
666 ntfree(socket);
667 }
668
669 int ri_append(struct rline *li, char *format, ...) {
670 char buf[MAX_BUFSIZE], escapedbuf[MAX_BUFSIZE * 2 + 1], *p, *tp;
671 int sizeleft = sizeof(li->buf) - (li->curpos - li->buf);
672 va_list ap;
673
674 va_start(ap, format);
675
676 if(vsnprintf(buf, sizeof(buf), format, ap) >= sizeof(buf)) {
677 va_end(ap);
678 return BF_OVER;
679 }
680
681 va_end(ap);
682
683 for(tp=escapedbuf,p=buf;*p||(*tp='\0');*tp++=*p++)
684 if((*p == ',') || (*p == '\\'))
685 *tp++ = '\\';
686
687 if(sizeleft > 0) {
688 if(li->curpos == li->buf) {
689 li->curpos+=snprintf(li->curpos, sizeleft, "%s", escapedbuf);
690 } else {
691 li->curpos+=snprintf(li->curpos, sizeleft, ",%s", escapedbuf);
692 }
693 }
694
695 if(sizeof(li->buf) - (li->curpos - li->buf) > 0) {
696 return BF_OK;
697 } else {
698 return BF_OVER;
699 }
700 }
701
702 int ri_error(struct rline *li, int error_code, char *format, ...) {
703 char buf[MAX_BUFSIZE], escapedbuf[MAX_BUFSIZE * 2 + 1], *p, *tp;
704 struct rline *pp, *lp = NULL;
705 va_list ap;
706 int retval = RE_OK;
707
708 if(li->socket) {
709 va_start(ap, format);
710 vsnprintf(buf, sizeof(buf), format, ap);
711 va_end(ap);
712
713 for(tp=escapedbuf,p=buf;*p||(*tp='\0');*tp++=*p++)
714 if((*p == ',') || (*p == '\\'))
715 *tp++ = '\\';
716
717 if(esocket_write_line(li->socket, "%d,OE%d,%s", li->id, error_code, escapedbuf))
718 retval = RE_SOCKET_ERROR;
719 }
720
721 for(pp=rlines;pp;lp=pp,pp=pp->next) {
722 if(pp == li) {
723 if(lp) {
724 lp->next = li->next;
725 } else {
726 rlines = li->next;
727 }
728 ntfree(li);
729 break;
730 }
731 }
732
733 return retval;
734 }
735
736 int ri_final(struct rline *li) {
737 struct rline *pp, *lp = NULL;
738 int retval = RE_OK;
739
740 if(li->socket)
741 if(esocket_write_line(li->socket, "%d,OO%s", li->id, li->buf))
742 retval = RE_SOCKET_ERROR;
743
744 for(pp=rlines;pp;lp=pp,pp=pp->next) {
745 if(pp == li) {
746 if(lp) {
747 lp->next = li->next;
748 } else {
749 rlines = li->next;
750 }
751 ntfree(li);
752 break;
753 }
754 }
755
756 return retval;
757 }
758
759 int ping_handler(struct rline *ri, int argc, char **argv) {
760 ri_append(ri, "OK");
761 return ri_final(ri);
762 }