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