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