chiark / gitweb /
doxygen: remove obsolete config file entries
[disorder] / lib / client.c
1 /*
2  * This file is part of DisOrder.
3  * Copyright (C) 2004-2010 Richard Kettlewell
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  * 
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  * 
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 /** @file lib/client.c
19  * @brief Simple C client
20  *
21  * See @ref lib/eclient.c for an asynchronous-capable client
22  * implementation.
23  */
24
25 #include "common.h"
26
27 #include <sys/types.h>
28 #include <sys/socket.h>
29 #include <netinet/in.h>
30 #include <sys/un.h>
31 #include <unistd.h>
32 #include <errno.h>
33 #include <netdb.h>
34 #include <pcre.h>
35
36 #include "log.h"
37 #include "mem.h"
38 #include "queue.h"
39 #include "client.h"
40 #include "charset.h"
41 #include "hex.h"
42 #include "split.h"
43 #include "vector.h"
44 #include "inputline.h"
45 #include "kvp.h"
46 #include "syscalls.h"
47 #include "printf.h"
48 #include "sink.h"
49 #include "addr.h"
50 #include "authhash.h"
51 #include "client-common.h"
52 #include "rights.h"
53 #include "kvp.h"
54
55 /** @brief Client handle contents */
56 struct disorder_client {
57   /** @brief Stream to read from */
58   FILE *fpin;
59   /** @brief Stream to write to */
60   FILE *fpout;
61   /** @brief Peer description */
62   char *ident;
63   /** @brief Username */
64   char *user;
65   /** @brief Report errors to @c stderr */
66   int verbose;
67   /** @brief Last error string */
68   const char *last;
69   /** @brief Address family */
70   int family;
71 };
72
73 /** @brief Create a new client
74  * @param verbose If nonzero, write extra junk to stderr
75  * @return Pointer to new client
76  *
77  * You must call disorder_connect(), disorder_connect_user() or
78  * disorder_connect_cookie() to connect it.  Use disorder_close() to
79  * dispose of the client when finished with it.
80  */
81 disorder_client *disorder_new(int verbose) {
82   disorder_client *c = xmalloc(sizeof (struct disorder_client));
83
84   c->verbose = verbose;
85   c->family = -1;
86   return c;
87 }
88
89 /** @brief Return the address family used by this client */
90 int disorder_client_af(disorder_client *c) {
91   return c->family;
92 }
93
94 /** @brief Read a response line
95  * @param c Client
96  * @param rp Where to store response, or NULL (UTF-8)
97  * @return Response code 0-999 or -1 on error
98  */
99 static int response(disorder_client *c, char **rp) {
100   char *r;
101
102   if(inputline(c->ident, c->fpin, &r, '\n')) {
103     byte_xasprintf((char **)&c->last, "input error: %s", strerror(errno));
104     return -1;
105   }
106   D(("response: %s", r));
107   if(rp)
108     *rp = r;
109   if(r[0] >= '0' && r[0] <= '9'
110      && r[1] >= '0' && r[1] <= '9'
111      && r[2] >= '0' && r[2] <= '9'
112      && r[3] == ' ') {
113     c->last = r + 4;
114     return (r[0] * 10 + r[1]) * 10 + r[2] - 111 * '0';
115   } else {
116     c->last = "invalid reply format";
117     disorder_error(0, "invalid reply format from %s", c->ident);
118     return -1;
119   }
120 }
121
122 /** @brief Return last response string
123  * @param c Client
124  * @return Last response string (UTF-8, English) or NULL
125  */
126 const char *disorder_last(disorder_client *c) {
127   return c->last;
128 }
129
130 /** @brief Read and partially parse a response
131  * @param c Client
132  * @param rp Where to store response text (or NULL) (UTF-8)
133  * @return 0 on success, non-0 on error
134  *
135  * 5xx responses count as errors.
136  *
137  * @p rp will NOT be filled in for xx9 responses (where it is just
138  * commentary for a command where it would normally be meaningful).
139  *
140  * NB that the response will NOT be converted to the local encoding.
141  */
142 static int check_response(disorder_client *c, char **rp) {
143   int rc;
144   char *r;
145
146   if((rc = response(c, &r)) == -1)
147     return -1;
148   else if(rc / 100 == 2) {
149     if(rp)
150       *rp = (rc % 10 == 9) ? 0 : xstrdup(r + 4);
151     xfree(r);
152     return 0;
153   } else {
154     if(c->verbose)
155       disorder_error(0, "from %s: %s", c->ident, utf82mb(r));
156     xfree(r);
157     return rc;
158   }
159 }
160
161 /** @brief Issue a command and parse a simple response
162  * @param c Client
163  * @param rp Where to store result, or NULL
164  * @param cmd Command
165  * @param ap Arguments (UTF-8), terminated by (char *)0
166  * @return 0 on success, non-0 on error
167  *
168  * 5xx responses count as errors.
169  *
170  * @p rp will NOT be filled in for xx9 responses (where it is just
171  * commentary for a command where it would normally be meaningful).
172  *
173  * NB that the response will NOT be converted to the local encoding
174  * nor will quotes be stripped.  See dequote().
175  *
176  * Put @ref disorder__body in the argument list followed by a char **
177  * and int giving the body to follow the command.  If the int is @c -1
178  * then the list is assumed to be NULL-terminated.  This may be used
179  * only once.
180  *
181  * Put @ref disorder__list in the argument list followed by a char **
182  * and int giving a list of arguments to include.  If the int is @c -1
183  * then the list is assumed to be NULL-terminated.  This may be used
184  * any number of times.
185  *
186  * Put @ref disorder__integer in the argument list followed by a long to
187  * send its value in decimal.  This may be used any number of times.
188  *
189  * Put @ref disorder__time in the argument list followed by a time_t
190  * to send its value in decimal.  This may be used any number of
191  * times.
192  *
193  * Usually you would call this via one of the following interfaces:
194  * - disorder_simple()
195  */
196 static int disorder_simple_v(disorder_client *c,
197                              char **rp,
198                              const char *cmd,
199                              va_list ap) {
200   const char *arg;
201   struct dynstr d;
202   char **body = NULL;
203   int nbody = 0;
204   int has_body = 0;
205
206   if(!c->fpout) {
207     c->last = "not connected";
208     disorder_error(0, "not connected to server");
209     return -1;
210   }
211   if(cmd) {
212     dynstr_init(&d);
213     dynstr_append_string(&d, cmd);
214     while((arg = va_arg(ap, const char *))) {
215       if(arg == disorder__body) {
216         body = va_arg(ap, char **);
217         nbody = va_arg(ap, int);
218         has_body = 1;
219       } else if(arg == disorder__list) {
220         char **list = va_arg(ap, char **);
221         int nlist = va_arg(ap, int);
222         if(nlist < 0) {
223           for(nlist = 0; list[nlist]; ++nlist)
224             ;
225         }
226         for(int n = 0; n < nlist; ++n) {
227           dynstr_append(&d, ' ');
228           dynstr_append_string(&d, quoteutf8(arg));
229         }
230       } else if(arg == disorder__integer) {
231         long n = va_arg(ap, long);
232         char buffer[16];
233         snprintf(buffer, sizeof buffer, "%ld", n);
234         dynstr_append(&d, ' ');
235         dynstr_append_string(&d, buffer);
236       } else if(arg == disorder__time) {
237         time_t n = va_arg(ap, time_t);
238         char buffer[16];
239         snprintf(buffer, sizeof buffer, "%lld", (long long)n);
240         dynstr_append(&d, ' ');
241         dynstr_append_string(&d, buffer);
242       } else {
243         dynstr_append(&d, ' ');
244         dynstr_append_string(&d, quoteutf8(arg));
245       }
246     }
247     dynstr_append(&d, '\n');
248     dynstr_terminate(&d);
249     D(("command: %s", d.vec));
250     if(fputs(d.vec, c->fpout) < 0)
251       goto write_error;
252     xfree(d.vec);
253     if(has_body) {
254       if(nbody < 0)
255         for(nbody = 0; body[nbody]; ++nbody)
256           ;
257       for(int n = 0; n < nbody; ++n) {
258         if(body[n][0] == '.')
259           if(fputc('.', c->fpout) < 0)
260             goto write_error;
261         if(fputs(body[n], c->fpout) < 0)
262           goto write_error;
263         if(fputc('\n', c->fpout) < 0)
264           goto write_error;
265       }
266       if(fputs(".\n", c->fpout) < 0)
267         goto write_error;
268     }
269     if(fflush(c->fpout))
270       goto write_error;
271   }
272   return check_response(c, rp);
273 write_error:
274   byte_xasprintf((char **)&c->last, "write error: %s", strerror(errno));
275   disorder_error(errno, "error writing to %s", c->ident);
276   return -1;
277 }
278
279 /** @brief Issue a command and parse a simple response
280  * @param c Client
281  * @param rp Where to store result, or NULL (UTF-8)
282  * @param cmd Command
283  * @return 0 on success, non-0 on error
284  *
285  * The remaining arguments are command arguments, terminated by (char
286  * *)0.  They should be in UTF-8.
287  *
288  * 5xx responses count as errors.
289  *
290  * @p rp will NOT be filled in for xx9 responses (where it is just
291  * commentary for a command where it would normally be meaningful).
292  *
293  * NB that the response will NOT be converted to the local encoding
294  * nor will quotes be stripped.  See dequote().
295  */
296 static int disorder_simple(disorder_client *c,
297                            char **rp,
298                            const char *cmd, ...) {
299   va_list ap;
300   int ret;
301
302   va_start(ap, cmd);
303   ret = disorder_simple_v(c, rp, cmd, ap);
304   va_end(ap);
305   return ret;
306 }
307
308 /** @brief Issue a command and split the response
309  * @param c Client
310  * @param vecp Where to store results
311  * @param nvecp Where to store count of results
312  * @param expected Expected count (or -1 to not check)
313  * @param cmd Command
314  * @return 0 on success, non-0 on error
315  *
316  * The remaining arguments are command arguments, terminated by (char
317  * *)0.  They should be in UTF-8.
318  *
319  * 5xx responses count as errors.
320  *
321  * @p rp will NOT be filled in for xx9 responses (where it is just
322  * commentary for a command where it would normally be meaningful).
323  *
324  * NB that the response will NOT be converted to the local encoding
325  * nor will quotes be stripped.  See dequote().
326  */
327 static int disorder_simple_split(disorder_client *c,
328                                  char ***vecp,
329                                  int *nvecp,
330                                  int expected,
331                                  const char *cmd, ...) {
332   va_list ap;
333   int ret;
334   char *r;
335   char **vec;
336   int nvec;
337
338   va_start(ap, cmd);
339   ret = disorder_simple_v(c, &r, cmd, ap);
340   va_end(ap);
341   if(!ret) {
342     vec = split(r, &nvec, SPLIT_QUOTES, 0, 0);
343     xfree(r);
344     if(expected < 0 || nvec == expected) {
345       *vecp = vec;
346       *nvecp = nvec;
347     } else {
348       disorder_error(0, "malformed reply to %s", cmd);
349       c->last = "malformed reply";
350       ret = -1;
351       free_strings(nvec, vec);
352     }
353   }
354   if(ret) {
355     *vecp = NULL;
356     *nvecp = 0;
357   }
358   return ret;
359 }
360
361 /** @brief Dequote a result string
362  * @param rc 0 on success, non-0 on error
363  * @param rp Where result string is stored (UTF-8)
364  * @return @p rc
365  *
366  * This is used as a wrapper around disorder_simple() to dequote
367  * results in place.
368  */
369 static int dequote(int rc, char **rp) {
370   char **rr;
371
372   if(!rc) {
373     if((rr = split(*rp, 0, SPLIT_QUOTES, 0, 0)) && *rr) {
374       xfree(*rp);
375       *rp = *rr;
376       xfree(rr);
377       return 0;
378     }
379     disorder_error(0, "invalid reply: %s", *rp);
380   }
381   return rc;
382 }
383
384 /** @brief Generic connection routine
385  * @param conf Configuration to follow
386  * @param c Client
387  * @param username Username to log in with or NULL
388  * @param password Password to log in with or NULL
389  * @param cookie Cookie to log in with or NULL
390  * @return 0 on success, non-0 on error
391  *
392  * @p cookie is tried first if not NULL.  If it is NULL then @p
393  * username must not be.  If @p username is not NULL then nor may @p
394  * password be.
395  */
396 int disorder_connect_generic(struct config *conf,
397                              disorder_client *c,
398                              const char *username,
399                              const char *password,
400                              const char *cookie) {
401   int fd = -1, fd2 = -1, nrvec = 0, rc;
402   unsigned char *nonce = NULL;
403   size_t nl;
404   char *res = NULL;
405   char *r = NULL, **rvec = NULL;
406   const char *protocol, *algorithm, *challenge;
407   struct sockaddr *sa = NULL;
408   socklen_t salen;
409
410   if((salen = find_server(conf, &sa, &c->ident)) == (socklen_t)-1)
411     return -1;
412   c->fpin = c->fpout = 0;
413   if((fd = socket(sa->sa_family, SOCK_STREAM, 0)) < 0) {
414     byte_xasprintf((char **)&c->last, "socket: %s", strerror(errno));
415     disorder_error(errno, "error calling socket");
416     return -1;
417   }
418   c->family = sa->sa_family;
419   if(connect(fd, sa, salen) < 0) {
420     byte_xasprintf((char **)&c->last, "connect: %s", strerror(errno));
421     disorder_error(errno, "error calling connect");
422     goto error;
423   }
424   if((fd2 = dup(fd)) < 0) {
425     byte_xasprintf((char **)&c->last, "dup: %s", strerror(errno));
426     disorder_error(errno, "error calling dup");
427     goto error;
428   }
429   if(!(c->fpin = fdopen(fd, "rb"))) {
430     byte_xasprintf((char **)&c->last, "fdopen: %s", strerror(errno));
431     disorder_error(errno, "error calling fdopen");
432     goto error;
433   }
434   fd = -1;
435   if(!(c->fpout = fdopen(fd2, "wb"))) {
436     byte_xasprintf((char **)&c->last, "fdopen: %s", strerror(errno));
437     disorder_error(errno, "error calling fdopen");
438     goto error;
439   }
440   fd2 = -1;
441   if((rc = disorder_simple(c, &r, 0, (const char *)0)))
442     goto error_rc;
443   if(!(rvec = split(r, &nrvec, SPLIT_QUOTES, 0, 0)))
444     goto error;
445   if(nrvec != 3) {
446     c->last = "cannot parse server greeting";
447     disorder_error(0, "cannot parse server greeting %s", r);
448     goto error;
449   }
450   protocol = rvec[0];
451   if(strcmp(protocol, "2")) {
452     c->last = "unknown protocol version";
453     disorder_error(0, "unknown protocol version: %s", protocol);
454     goto error;
455   }
456   algorithm = rvec[1];
457   challenge = rvec[2];
458   if(!(nonce = unhex(challenge, &nl)))
459     goto error;
460   if(cookie) {
461     if(!dequote(disorder_simple(c, &c->user, "cookie", cookie, (char *)0),
462                 &c->user))
463       return 0;                         /* success */
464     if(!username) {
465       c->last = "cookie failed and no username";
466       disorder_error(0, "cookie did not work and no username available");
467       goto error;
468     }
469   }
470   if(!(res = authhash(nonce, nl, password, algorithm))) {
471     c->last = "error computing authorization hash";
472     goto error;
473   }
474   if((rc = disorder_simple(c, 0, "user", username, res, (char *)0)))
475     goto error_rc;
476   c->user = xstrdup(username);
477   xfree(res);
478   free_strings(nrvec, rvec);
479   xfree(nonce);
480   xfree(sa);
481   xfree(r);
482   return 0;
483 error:
484   rc = -1;
485 error_rc:
486   if(c->fpin) {
487     fclose(c->fpin);
488     c->fpin = 0;
489   }
490   if(c->fpout) {
491     fclose(c->fpout);
492     c->fpout = 0;
493   }
494   if(fd2 != -1) close(fd2);
495   if(fd != -1) close(fd);
496   return rc;
497 }
498
499 /** @brief Connect a client with a specified username and password
500  * @param c Client
501  * @param username Username to log in with
502  * @param password Password to log in with
503  * @return 0 on success, non-0 on error
504  */
505 int disorder_connect_user(disorder_client *c,
506                           const char *username,
507                           const char *password) {
508   return disorder_connect_generic(config,
509                                   c,
510                                   username,
511                                   password,
512                                   0);
513 }
514
515 /** @brief Connect a client
516  * @param c Client
517  * @return 0 on success, non-0 on error
518  *
519  * The connection will use the username and password found in @ref
520  * config, or directly from the database if no password is found and
521  * the database is readable (usually only for root).
522  */
523 int disorder_connect(disorder_client *c) {
524   const char *username, *password;
525
526   if(!(username = config->username)) {
527     c->last = "no username";
528     disorder_error(0, "no username configured");
529     return -1;
530   }
531   password = config->password;
532   /* If we're connecting as 'root' guess that we're the system root
533    * user (or the jukebox user), both of which can use the privileged
534    * socket.  They can also furtle with the db directly: that is why
535    * privileged socket does not represent a privilege escalation. */
536   if(!password
537      && !strcmp(username, "root"))
538     password = "anything will do for root";
539   if(!password) {
540     /* Oh well */
541     c->last = "no password";
542     disorder_error(0, "no password configured for user '%s'", username);
543     return -1;
544   }
545   return disorder_connect_generic(config,
546                                   c,
547                                   username,
548                                   password,
549                                   0);
550 }
551
552 /** @brief Connect a client
553  * @param c Client
554  * @param cookie Cookie to log in with, or NULL
555  * @return 0 on success, non-0 on error
556  *
557  * If @p cookie is NULL or does not work then we attempt to log in as
558  * guest instead (so when the cookie expires only an extra round trip
559  * is needed rathre than a complete new login).
560  */
561 int disorder_connect_cookie(disorder_client *c,
562                             const char *cookie) {
563   return disorder_connect_generic(config,
564                                   c,
565                                   "guest",
566                                   "",
567                                   cookie);
568 }
569
570 /** @brief Close a client
571  * @param c Client
572  * @return 0 on succcess, non-0 on errior
573  *
574  * The client is still closed even on error.  It might well be
575  * appropriate to ignore the return value.
576  */
577 int disorder_close(disorder_client *c) {
578   int ret = 0;
579
580   if(c->fpin) {
581     if(fclose(c->fpin) < 0) {
582       byte_xasprintf((char **)&c->last, "fclose: %s", strerror(errno));
583       disorder_error(errno, "error calling fclose");
584       ret = -1;
585     }
586     c->fpin = 0;
587   }
588   if(c->fpout) {
589     if(fclose(c->fpout) < 0) {
590       byte_xasprintf((char **)&c->last, "fclose: %s", strerror(errno));
591       disorder_error(errno, "error calling fclose");
592       ret = -1;
593     }
594     c->fpout = 0;
595   }
596   xfree(c->ident);
597   c->ident = 0;
598   xfree(c->user);
599   c->user = 0;
600   return ret;
601 }
602
603 static void client_error(const char *msg,
604                          void attribute((unused)) *u) {
605   disorder_error(0, "error parsing reply: %s", msg);
606 }
607
608 /** @brief Get a single queue entry
609  * @param c Client
610  * @param cmd Command
611  * @param qp Where to store track information
612  * @return 0 on success, non-0 on error
613  */
614 static int onequeue(disorder_client *c, const char *cmd,
615                     struct queue_entry **qp) {
616   char *r;
617   struct queue_entry *q;
618   int rc;
619
620   if((rc = disorder_simple(c, &r, cmd, (char *)0)))
621     return rc;
622   if(r) {
623     q = xmalloc(sizeof *q);
624     if(queue_unmarshall(q, r, client_error, 0))
625       return -1;
626     *qp = q;
627   } else
628     *qp = 0;
629   return 0;
630 }
631
632 /** @brief Fetch the queue, recent list, etc */
633 static int readqueue(disorder_client *c,
634                      struct queue_entry **qp) {
635   struct queue_entry *qh, **qt = &qh, *q;
636   char *l;
637
638   while(inputline(c->ident, c->fpin, &l, '\n') >= 0) {
639     if(!strcmp(l, ".")) {
640       *qt = 0;
641       *qp = qh;
642       xfree(l);
643       return 0;
644     }
645     q = xmalloc(sizeof *q);
646     if(!queue_unmarshall(q, l, client_error, 0)) {
647       *qt = q;
648       qt = &q->next;
649     }
650     xfree(l);
651   }
652   if(ferror(c->fpin)) {
653     byte_xasprintf((char **)&c->last, "input error: %s", strerror(errno));
654     disorder_error(errno, "error reading %s", c->ident);
655   } else {
656     c->last = "input error: unexpected EOF";
657     disorder_error(0, "error reading %s: unexpected EOF", c->ident);
658   }
659   return -1;
660 }
661
662 /** @brief Read a dot-stuffed list
663  * @param c Client
664  * @param vecp Where to store list (UTF-8)
665  * @param nvecp Where to store number of items, or NULL
666  * @return 0 on success, non-0 on error
667  *
668  * The list will have a final NULL not counted in @p nvecp.
669  */
670 static int readlist(disorder_client *c, char ***vecp, int *nvecp) {
671   char *l;
672   struct vector v;
673
674   vector_init(&v);
675   while(inputline(c->ident, c->fpin, &l, '\n') >= 0) {
676     if(!strcmp(l, ".")) {
677       vector_terminate(&v);
678       if(nvecp)
679         *nvecp = v.nvec;
680       *vecp = v.vec;
681       xfree(l);
682       return 0;
683     }
684     vector_append(&v, xstrdup(l + (*l == '.')));
685     xfree(l);
686   }
687   if(ferror(c->fpin)) {
688     byte_xasprintf((char **)&c->last, "input error: %s", strerror(errno));
689     disorder_error(errno, "error reading %s", c->ident);
690   } else {
691     c->last = "input error: unexpxected EOF";
692     disorder_error(0, "error reading %s: unexpected EOF", c->ident);
693   }
694   return -1;
695 }
696
697 /** @brief Return the user we logged in with
698  * @param c Client
699  * @return User name (owned by @p c, don't modify)
700  */
701 char *disorder_user(disorder_client *c) {
702   return c->user;
703 }
704
705 static void pairlist_error_handler(const char *msg,
706                                void attribute((unused)) *u) {
707   disorder_error(0, "error handling key-value pair reply: %s", msg);
708 }
709
710 /** @brief Get a list of key-value pairs
711  * @param c Client
712  * @param kp Where to store linked list of preferences
713  * @param cmd Command
714  * @param ... Arguments
715  * @return 0 on success, non-0 on error
716  */
717 static int pairlist(disorder_client *c, struct kvp **kp, const char *cmd, ...) {
718   char **vec, **pvec;
719   int nvec, npvec, n, rc;
720   struct kvp *k;
721   va_list ap;
722
723   va_start(ap, cmd);
724   rc = disorder_simple_v(c, 0, cmd, ap);
725   va_end(ap);
726   if(rc)
727     return rc;
728   if((rc = readlist(c, &vec, &nvec)))
729      return rc;
730   for(n = 0; n < nvec; ++n) {
731     if(!(pvec = split(vec[n], &npvec, SPLIT_QUOTES, pairlist_error_handler, 0)))
732       return -1;
733     if(npvec != 2) {
734       pairlist_error_handler("malformed response", 0);
735       return -1;
736     }
737     *kp = k = xmalloc(sizeof *k);
738     k->name = pvec[0];
739     k->value = pvec[1];
740     kp = &k->next;
741     xfree(pvec);
742   }
743   free_strings(nvec, vec);
744   *kp = 0;
745   return 0;
746 }
747
748 /** @brief Parse a boolean response
749  * @param cmd Command for use in error messsage
750  * @param value Result from server
751  * @param flagp Where to store result
752  * @return 0 on success, non-0 on error
753  */
754 static int boolean(const char *cmd, const char *value,
755                    int *flagp) {
756   if(!strcmp(value, "yes")) *flagp = 1;
757   else if(!strcmp(value, "no")) *flagp = 0;
758   else {
759     disorder_error(0, "malformed response to '%s'", cmd);
760     return -1;
761   }
762   return 0;
763 }
764
765 /** @brief Log to a sink
766  * @param c Client
767  * @param s Sink to write log lines to
768  * @return 0 on success, non-0 on error
769  */
770 int disorder_log(disorder_client *c, struct sink *s) {
771   char *l;
772   int rc;
773     
774   if((rc = disorder_simple(c, 0, "log", (char *)0)))
775     return rc;
776   while(inputline(c->ident, c->fpin, &l, '\n') >= 0 && strcmp(l, "."))
777     if(sink_printf(s, "%s\n", l) < 0) return -1;
778   if(ferror(c->fpin) || feof(c->fpin)) {
779     byte_xasprintf((char **)&c->last, "input error: %s",
780                    ferror(c->fpin) ? strerror(errno) : "unexpxected EOF");
781     return -1;
782   }
783   return 0;
784 }
785
786 #include "client-stubs.c"
787
788 /*
789 Local Variables:
790 c-basic-offset:2
791 comment-column:40
792 End:
793 */