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