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