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