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