CtdlEncodeBase64() - only add linebreaks if told to by the caller.
[citadel.git] / webcit-ng / ctdlclient.c
1 // Functions that handle communication with a Citadel Server
2 //
3 // Copyright (c) 1987-2022 by the citadel.org team
4 //
5 // This program is open source software.  It runs great on the
6 // Linux operating system (and probably elsewhere).  You can use,
7 // copy, and run it under the terms of the GNU General Public
8 // License version 3.
9 //
10 // This program is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
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 // Read a specific number of bytes of binary data from the Citadel server.
22 // Returns the number of bytes read or -1 for error.
23 int ctdl_read_binary(struct ctdlsession *ctdl, char *buf, int bytes_requested) {
24         int bytes_read = 0;
25         int c = 0;
26
27         while (bytes_read < bytes_requested) {
28                 c = read(ctdl->sock, &buf[bytes_read], bytes_requested-bytes_read);
29                 if (c <= 0) {
30                         syslog(LOG_DEBUG, "Socket error or zero-length read");
31                         return (-1);
32                 }
33                 bytes_read += c;
34         }
35         return (bytes_read);
36 }
37
38
39 // Read a newline-terminated line of text from the Citadel server.
40 // Returns the string length or -1 for error.
41 int ctdl_readline(struct ctdlsession *ctdl, char *buf, int maxbytes) {
42         int len = 0;
43         int c = 0;
44
45         if (buf == NULL) {
46                 return (-1);
47         }
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[32m[ %s\033[0m", buf);
61                         return (len);
62                 }
63                 ++len;
64         }
65         // syslog(LOG_DEBUG, "\033[32m[ %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)), 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 // Extract from the headers, the username and password the client is attempting to use.
136 // This could be HTTP AUTH or it could be in the cookies.
137 void extract_auth(struct http_transaction *h, char *authbuf, int authbuflen) {
138         if (authbuf == NULL) {
139                 return;
140         }
141
142         memset(authbuf, 0, authbuflen);
143
144         char *authheader = header_val(h, "Authorization");
145         if (authheader) {
146                 if (!strncasecmp(authheader, "Basic ", 6)) {
147                         safestrncpy(authbuf, &authheader[6], authbuflen);
148                         return;         // HTTP-AUTH was found -- stop here
149                 }
150         }
151
152         char *cookieheader = header_val(h, "Cookie");
153         if (cookieheader) {
154                 char *wcauth = strstr(cookieheader, "wcauth=");
155                 if (wcauth) {
156                         safestrncpy(authbuf, &cookieheader[7], authbuflen);
157                         char *semicolon = strchr(authbuf, ';');
158                         if (semicolon != NULL) {
159                                 *semicolon = 0;
160                         }
161                         if (strlen(authbuf) < 3) {      // impossibly small
162                                 authbuf[0] = 0;
163                         }
164                         return;         // Cookie auth was found -- stop here
165                 }
166         }
167         // no authorization found in headers ... this is an anonymous session
168 }
169
170
171 // Log in to the Citadel server.  Returns 0 on success or nonzero on error.
172 //
173 // 'auth' should be a base64-encoded "username:password" combination (like in http-auth)
174 //
175 // If 'resultbuf' is not NULL, it should be a buffer of at least 1024 characters,
176 // and will be filled with the result from a Citadel server command.
177 int login_to_citadel(struct ctdlsession *c, char *auth, char *resultbuf) {
178         char localbuf[1024];
179         char *buf;
180         int buflen;
181         char supplied_username[AUTH_MAX];
182         char supplied_password[AUTH_MAX];
183
184         if (resultbuf != NULL) {
185                 buf = resultbuf;
186         }
187         else {
188                 buf = localbuf;
189         }
190
191         buflen = CtdlDecodeBase64(buf, auth, strlen(auth));
192         extract_token(supplied_username, buf, 0, ':', sizeof supplied_username);
193         extract_token(supplied_password, buf, 1, ':', sizeof supplied_password);
194         syslog(LOG_DEBUG, "Supplied credentials: username=%s, password=(%d bytes)", supplied_username, (int) strlen(supplied_password));
195
196         ctdl_printf(c, "USER %s", supplied_username);
197         ctdl_readline(c, buf, 1024);
198         if (buf[0] != '3') {
199                 syslog(LOG_DEBUG, "No such user: %s", buf);
200                 return(1);      // no such user; resultbuf will explain why
201         }
202
203         ctdl_printf(c, "PASS %s", supplied_password);
204         ctdl_readline(c, buf, 1024);
205
206         if (buf[0] == '2') {
207                 extract_token(c->whoami, &buf[4], 0, '|', sizeof c->whoami);
208                 syslog(LOG_DEBUG, "Logged in as %s", c->whoami);
209
210                 // Re-encode the auth string so it contains the properly formatted username
211                 char new_auth_string[1024];
212                 snprintf(new_auth_string, sizeof(new_auth_string),  "%s:%s", c->whoami, supplied_password);
213                 CtdlEncodeBase64(c->auth, new_auth_string, strlen(new_auth_string), BASE64_NO_LINEBREAKS);
214
215                 return(0);
216         }
217
218         syslog(LOG_DEBUG, "Login failed: %s", &buf[4]);
219         return(1);              // login failed; resultbuf will explain why
220 }
221
222
223 // This is a variant of the "server connection pool" design pattern.  We go through our list
224 // of connections to Citadel Server, looking for a connection that is at once:
225 // 1. Not currently serving a WebCit transaction (is_bound)
226 // 2a. Is logged in to Citadel as the correct user, if the HTTP session is logged in; or
227 // 2b. Is NOT logged in to Citadel, if the HTTP session is not logged in.
228 // If we find a qualifying connection, we bind to it for the duration of this WebCit HTTP transaction.
229 // Otherwise, we create a new connection to Citadel Server and add it to the pool.
230 struct ctdlsession *connect_to_citadel(struct http_transaction *h) {
231         struct ctdlsession *cptr = NULL;
232         struct ctdlsession *my_session = NULL;
233         int is_new_session = 0;
234         char buf[1024];
235         char auth[AUTH_MAX];
236         int r = 0;
237
238         // Does the request carry a username and password?
239         extract_auth(h, auth, sizeof auth);
240
241         // Lock the connection pool while we claim our connection
242         pthread_mutex_lock(&cpool_mutex);
243         if (cpool != NULL) {
244                 for (cptr = cpool; ((cptr != NULL) && (my_session == NULL)); cptr = cptr->next) {
245                         if ((cptr->is_bound == 0) && (!strcmp(cptr->auth, auth))) {
246                                 my_session = cptr;
247                                 my_session->is_bound = 1;
248                         }
249                 }
250         }
251         if (my_session == NULL) {
252                 syslog(LOG_DEBUG, "No qualifying sessions , starting a new one");
253                 my_session = malloc(sizeof(struct ctdlsession));
254                 if (my_session != NULL) {
255                         memset(my_session, 0, sizeof(struct ctdlsession));
256                         is_new_session = 1;
257                         my_session->next = cpool;
258                         cpool = my_session;
259                         my_session->is_bound = 1;
260                 }
261         }
262         pthread_mutex_unlock(&cpool_mutex);
263         if (my_session == NULL) {
264                 return(NULL);                                           // Could not create a new session (yikes!)
265         }
266
267         if (my_session->sock < 3) {
268                 is_new_session = 1;
269         }
270         else {                                                          // make sure our Citadel session is still working
271                 int test_conn;
272                 test_conn = ctdl_write(my_session, HKEY("NOOP\n"));
273                 if (test_conn < 5) {
274                         syslog(LOG_DEBUG, "Citadel session is broken , must reconnect");
275                         close(my_session->sock);
276                         my_session->sock = 0;
277                         is_new_session = 1;
278                 }
279                 else {
280                         test_conn = ctdl_readline(my_session, buf, sizeof(buf));
281                         if (test_conn < 1) {
282                                 syslog(LOG_DEBUG, "Citadel session is broken , must reconnect");
283                                 close(my_session->sock);
284                                 my_session->sock = 0;
285                                 is_new_session = 1;
286                         }
287                 }
288         }
289
290         if (is_new_session) {
291                 strcpy(my_session->room, "");
292                 static char *ctdl_sock_path = NULL;
293                 if (!ctdl_sock_path) {
294                         ctdl_sock_path = malloc(PATH_MAX);
295                         snprintf(ctdl_sock_path, PATH_MAX, "%s/citadel.socket", ctdl_dir);
296                 }
297                 my_session->sock = uds_connectsock(ctdl_sock_path);
298                 ctdl_readline(my_session, buf, sizeof(buf));            // skip past the server greeting banner
299
300                 if (!IsEmptyStr(auth)) {                                // do we need to log in to Citadel?
301                         r = login_to_citadel(my_session, auth, NULL);   // FIXME figure out what happens if login failed
302                 }
303         }
304
305         ctdl_printf(my_session, "NOOP");
306         ctdl_readline(my_session, buf, sizeof(buf));
307         my_session->last_access = time(NULL);
308         ++my_session->num_requests_handled;
309         return(my_session);
310 }
311
312
313 // Release our Citadel Server connection back into the pool.
314 void disconnect_from_citadel(struct ctdlsession *ctdl) {
315         pthread_mutex_lock(&cpool_mutex);
316         ctdl->is_bound = 0;
317         pthread_mutex_unlock(&cpool_mutex);
318 }