Mailing list header changes (fuck you Google)
[citadel.git] / webcit-ng / ctdlclient.c
1 /*
2  * Functions that handle communication with a Citadel Server
3  *
4  * Copyright (c) 1987-2018 by the citadel.org team
5  *
6  * This program is open source software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License version 3.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  */
14
15 #include "webcit.h"
16
17 struct ctdlsession *cpool = NULL;                               // linked list of connections to the Citadel server
18 pthread_mutex_t cpool_mutex = PTHREAD_MUTEX_INITIALIZER;        // Lock it before modifying
19
20
21 /*
22  * Read a specific number of bytes of binary data from the Citadel server.
23  * Returns the number of bytes read or -1 for error.
24  */
25 int ctdl_read_binary(struct ctdlsession *ctdl, char *buf, int bytes_requested)
26 {
27         int bytes_read = 0;
28         int c = 0;
29
30         while (bytes_read < bytes_requested) {
31                 c = read(ctdl->sock, &buf[bytes_read], bytes_requested-bytes_read);
32                 if (c <= 0) {
33                         syslog(LOG_DEBUG, "Socket error or zero-length read");
34                         return (-1);
35                 }
36                 bytes_read += c;
37         }
38         return (bytes_read);
39 }
40
41
42 /*
43  * Read a newline-terminated line of text from the Citadel server.
44  * Returns the string length or -1 for error.
45  */
46 int ctdl_readline(struct ctdlsession *ctdl, char *buf, int maxbytes)
47 {
48         int len = 0;
49         int c = 0;
50
51         if (buf == NULL)
52                 return (-1);
53
54         while (len < maxbytes) {
55                 c = read(ctdl->sock, &buf[len], 1);
56                 if (c <= 0) {
57                         syslog(LOG_DEBUG, "Socket error or zero-length read");
58                         return (-1);
59                 }
60                 if (buf[len] == '\n') {
61                         if ((len > 0) && (buf[len - 1] == '\r')) {
62                                 --len;
63                         }
64                         buf[len] = 0;
65                         // syslog(LOG_DEBUG, "\033[33m[ %s\033[0m", buf);
66                         return (len);
67                 }
68                 ++len;
69         }
70         // syslog(LOG_DEBUG, "\033[33m[ %s\033[0m", buf);
71         return (len);
72 }
73
74
75 /*
76  * Read lines of text from the Citadel server until a 000 terminator is received.
77  * Implemented in terms of ctdl_readline() and is therefore transparent...
78  * Returns a newly allocated StrBuf or NULL for error.
79  */
80 StrBuf *ctdl_readtextmsg(struct ctdlsession * ctdl)
81 {
82         char buf[1024];
83         StrBuf *sj = NewStrBuf();
84         if (!sj) {
85                 return NULL;
86         }
87
88         while ((ctdl_readline(ctdl, buf, sizeof(buf)) >= 0) && (strcmp(buf, "000"))) {
89                 StrBufAppendPrintf(sj, "%s\n", buf);
90         }
91
92         return sj;
93 }
94
95
96 /*
97  * Write to the Citadel server.  For now we're just wrapping write() in case we
98  * need to add anything else later.
99  */
100 ssize_t ctdl_write(struct ctdlsession * ctdl, const void *buf, size_t count)
101 {
102         return write(ctdl->sock, buf, count);
103 }
104
105
106 /*
107  * printf() type function to send data to the Citadel Server.
108  */
109 void ctdl_printf(struct ctdlsession *ctdl, const char *format, ...)
110 {
111         va_list arg_ptr;
112         StrBuf *Buf = NewStrBuf();
113
114         va_start(arg_ptr, format);
115         StrBufVAppendPrintf(Buf, format, arg_ptr);
116         va_end(arg_ptr);
117
118         syslog(LOG_DEBUG, "\033[32m] %s\033[0m", ChrPtr(Buf));
119         ctdl_write(ctdl, (char *) ChrPtr(Buf), StrLength(Buf));
120         ctdl_write(ctdl, "\n", 1);
121         FreeStrBuf(&Buf);
122 }
123
124
125 /*
126  * Client side - connect to a unix domain socket
127  */
128 int uds_connectsock(char *sockpath)
129 {
130         struct sockaddr_un addr;
131         int s;
132
133         memset(&addr, 0, sizeof(addr));
134         addr.sun_family = AF_UNIX;
135         strncpy(addr.sun_path, sockpath, sizeof addr.sun_path);
136
137         s = socket(AF_UNIX, SOCK_STREAM, 0);
138         if (s < 0) {
139                 syslog(LOG_WARNING, "Can't create socket [%s]: %s", sockpath, strerror(errno));
140                 return (-1);
141         }
142
143         if (connect(s, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
144                 syslog(LOG_WARNING, "Can't connect [%s]: %s", sockpath, strerror(errno));
145                 close(s);
146                 return (-1);
147         }
148         return s;
149 }
150
151
152 /*
153  * TCP client - connect to a host/port 
154  */
155 int tcp_connectsock(char *host, char *service)
156 {
157         struct in6_addr serveraddr;
158         struct addrinfo hints;
159         struct addrinfo *res = NULL;
160         struct addrinfo *ai = NULL;
161         int rc = (-1);
162         int s = (-1);
163
164         if ((host == NULL) || IsEmptyStr(host))
165                 return (-1);
166         if ((service == NULL) || IsEmptyStr(service))
167                 return (-1);
168
169         syslog(LOG_DEBUG, "tcp_connectsock(%s,%s)", host, service);
170
171         memset(&hints, 0x00, sizeof(hints));
172         hints.ai_flags = AI_NUMERICSERV;
173         hints.ai_family = AF_UNSPEC;
174         hints.ai_socktype = SOCK_STREAM;
175
176         /*
177          * Handle numeric IPv4 and IPv6 addresses
178          */
179         rc = inet_pton(AF_INET, host, &serveraddr);
180         if (rc == 1) {          /* dotted quad */
181                 hints.ai_family = AF_INET;
182                 hints.ai_flags |= AI_NUMERICHOST;
183         } else {
184                 rc = inet_pton(AF_INET6, host, &serveraddr);
185                 if (rc == 1) {  /* IPv6 address */
186                         hints.ai_family = AF_INET6;
187                         hints.ai_flags |= AI_NUMERICHOST;
188                 }
189         }
190
191         /* Begin the connection process */
192
193         rc = getaddrinfo(host, service, &hints, &res);
194         if (rc != 0) {
195                 syslog(LOG_DEBUG, "%s: %s", host, gai_strerror(rc));
196                 freeaddrinfo(res);
197                 return (-1);
198         }
199
200         /*
201          * Try all available addresses until we connect to one or until we run out.
202          */
203         for (ai = res; ai != NULL; ai = ai->ai_next) {
204
205                 if (ai->ai_family == AF_INET)
206                         syslog(LOG_DEBUG, "Trying IPv4");
207                 else if (ai->ai_family == AF_INET6)
208                         syslog(LOG_DEBUG, "Trying IPv6");
209                 else
210                         syslog(LOG_WARNING, "This is going to fail.");
211
212                 s = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
213                 if (s < 0) {
214                         syslog(LOG_WARNING, "socket() failed: %s", strerror(errno));
215                         freeaddrinfo(res);
216                         return (-1);
217                 }
218                 rc = connect(s, ai->ai_addr, ai->ai_addrlen);
219                 if (rc >= 0) {
220                         int fdflags;
221                         freeaddrinfo(res);
222
223                         fdflags = fcntl(rc, F_GETFL);
224                         if (fdflags < 0) {
225                                 syslog(LOG_ERR, "unable to get socket %d flags! %s", rc, strerror(errno));
226                                 close(rc);
227                                 return -1;
228                         }
229                         fdflags = fdflags | O_NONBLOCK;
230                         if (fcntl(rc, F_SETFL, fdflags) < 0) {
231                                 syslog(LOG_ERR, "unable to set socket %d nonblocking flags! %s", rc, strerror(errno));
232                                 close(s);
233                                 return -1;
234                         }
235
236                         return (s);
237                 } else {
238                         syslog(LOG_WARNING, "connect() failed: %s", strerror(errno));
239                         close(s);
240                 }
241         }
242         freeaddrinfo(res);
243         return (-1);
244 }
245
246
247 /*
248  * Extract from the headers, the username and password the client is attempting to use.
249  * This could be HTTP AUTH or it could be in the cookies.
250  */
251 void extract_auth(struct http_transaction *h, char *authbuf, int authbuflen)
252 {
253         if (authbuf == NULL)
254                 return;
255         authbuf[0] = 0;
256
257         char *authheader = header_val(h, "Authorization");
258         if (authheader) {
259                 if (!strncasecmp(authheader, "Basic ", 6)) {
260                         safestrncpy(authbuf, &authheader[6], authbuflen);
261                         return; // HTTP-AUTH was found -- stop here
262                 }
263         }
264
265         char *cookieheader = header_val(h, "Cookie");
266         if (cookieheader) {
267                 char *wcauth = strstr(cookieheader, "wcauth=");
268                 if (wcauth) {
269                         safestrncpy(authbuf, &cookieheader[7], authbuflen);
270                         char *semicolon = strchr(authbuf, ';');
271                         if (semicolon != NULL) {
272                                 *semicolon = 0;
273                         }
274                         if (strlen(authbuf) < 3) {      // impossibly small
275                                 authbuf[0] = 0;
276                         }
277                         return; // Cookie auth was found -- stop here
278                 }
279         }
280         // no authorization found in headers ... this is an anonymous session
281 }
282
283
284 /*
285  * Log in to the Citadel server.  Returns 0 on success or nonzero on error.
286  *
287  * 'auth' should be a base64-encoded "username:password" combination (like in http-auth)
288  *
289  * If 'resultbuf' is not NULL, it should be a buffer of at least 1024 characters,
290  * and will be filled with the result from a Citadel server command.
291  */
292 int login_to_citadel(struct ctdlsession *c, char *auth, char *resultbuf)
293 {
294         char localbuf[1024];
295         char *buf;
296         int buflen;
297         char supplied_username[AUTH_MAX];
298         char supplied_password[AUTH_MAX];
299
300         if (resultbuf != NULL) {
301                 buf = resultbuf;
302         } else {
303                 buf = localbuf;
304         }
305
306         buflen = CtdlDecodeBase64(buf, auth, strlen(auth));
307         extract_token(supplied_username, buf, 0, ':', sizeof supplied_username);
308         extract_token(supplied_password, buf, 1, ':', sizeof supplied_password);
309         syslog(LOG_DEBUG, "Supplied credentials: username=%s, pwlen=%d", supplied_username, (int) strlen(supplied_password));
310
311         ctdl_printf(c, "USER %s", supplied_username);
312         ctdl_readline(c, buf, 1024);
313         if (buf[0] != '3') {
314                 syslog(LOG_DEBUG, "No such user: %s", buf);
315                 return (1);     // no such user; resultbuf will explain why
316         }
317
318         ctdl_printf(c, "PASS %s", supplied_password);
319         ctdl_readline(c, buf, 1024);
320
321         if (buf[0] == '2') {
322                 strcpy(c->auth, auth);
323                 extract_token(c->whoami, &buf[4], 0, '|', sizeof c->whoami);
324                 syslog(LOG_DEBUG, "Login succeeded: %s", buf);
325                 return (0);
326         }
327
328         syslog(LOG_DEBUG, "Login failed: %s", buf);
329         return (1);             // login failed; resultbuf will explain why
330 }
331
332
333 /*
334  * Hunt for, or create, a connection to our Citadel Server
335  */
336 struct ctdlsession *connect_to_citadel(struct http_transaction *h)
337 {
338         struct ctdlsession *cptr = NULL;
339         struct ctdlsession *my_session = NULL;
340         int is_new_session = 0;
341         char buf[1024];
342         char auth[AUTH_MAX];
343         int r = 0;
344
345         // Does the request carry a username and password?
346         extract_auth(h, auth, sizeof auth);
347         syslog(LOG_DEBUG, "Session auth: %s", auth);    // remove this log when development is done
348
349         // Lock the connection pool while we claim our connection
350         pthread_mutex_lock(&cpool_mutex);
351         if (cpool != NULL)
352                 for (cptr = cpool; ((cptr != NULL) && (my_session == NULL)); cptr = cptr->next) {
353                         if ((cptr->is_bound == 0) && (!strcmp(cptr->auth, auth))) {
354                                 my_session = cptr;
355                                 my_session->is_bound = 1;
356                         }
357                 }
358         if (my_session == NULL) {
359                 syslog(LOG_DEBUG, "No qualifying sessions , starting a new one");
360                 my_session = malloc(sizeof(struct ctdlsession));
361                 if (my_session != NULL) {
362                         memset(my_session, 0, sizeof(struct ctdlsession));
363                         is_new_session = 1;
364                         my_session->next = cpool;
365                         cpool = my_session;
366                         my_session->is_bound = 1;
367                 }
368         }
369         pthread_mutex_unlock(&cpool_mutex);
370         if (my_session == NULL) {
371                 return (NULL);  // oh well
372         }
373
374         if (my_session->sock < 3) {
375                 is_new_session = 1;
376         } else {                // make sure our Citadel session is still good
377                 int test_conn;
378                 test_conn = ctdl_write(my_session, HKEY("NOOP\n"));
379                 if (test_conn < 5) {
380                         syslog(LOG_DEBUG, "Citadel session is broken , must reconnect");
381                         close(my_session->sock);
382                         my_session->sock = 0;
383                         is_new_session = 1;
384                 } else {
385                         test_conn = ctdl_readline(my_session, buf, sizeof(buf));
386                         if (test_conn < 1) {
387                                 syslog(LOG_DEBUG, "Citadel session is broken , must reconnect");
388                                 close(my_session->sock);
389                                 my_session->sock = 0;
390                                 is_new_session = 1;
391                         }
392                 }
393         }
394
395         if (is_new_session) {
396                 strcpy(my_session->room, "");
397                 my_session->sock = tcp_connectsock(ctdlhost, ctdlport);
398                 ctdl_readline(my_session, buf, sizeof(buf));            // skip past the server greeting banner
399
400                 if (!IsEmptyStr(auth)) {                                // do we need to log in to Citadel?
401                         r = login_to_citadel(my_session, auth, NULL);   // FIXME figure out what happens if login failed
402                 }
403         }
404         ctdl_printf(my_session, "NOOP");
405         ctdl_readline(my_session, buf, sizeof(buf));
406         my_session->last_access = time(NULL);
407         ++my_session->num_requests_handled;
408
409         return (my_session);
410 }
411
412
413 /*
414  * Release our Citadel Server connection back into the pool.
415  */
416 void disconnect_from_citadel(struct ctdlsession *ctdl)
417 {
418         pthread_mutex_lock(&cpool_mutex);
419         ctdl->is_bound = 0;
420         pthread_mutex_unlock(&cpool_mutex);
421 }