55ddb19b812d345216c82c465ae39520d8820801
[citadel.git] / webcit-ng / server / 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.  Use, duplication, or
6 // disclosure are subject to the GNU General Public License v3.
7
8 #include "webcit.h"
9
10 struct ctdlsession *cpool = NULL;                               // linked list of connections to the Citadel server
11 pthread_mutex_t cpool_mutex = PTHREAD_MUTEX_INITIALIZER;        // Lock it before modifying
12
13
14 // Read a specific number of bytes of binary data from the Citadel server.
15 // Returns the number of bytes read or -1 for error.
16 int ctdl_read_binary(struct ctdlsession *ctdl, char *buf, int bytes_requested) {
17         int bytes_read = 0;
18         int c = 0;
19
20         while (bytes_read < bytes_requested) {
21                 c = read(ctdl->sock, &buf[bytes_read], bytes_requested-bytes_read);
22                 if (c <= 0) {
23                         syslog(LOG_DEBUG, "Socket error or zero-length read");
24                         return (-1);
25                 }
26                 bytes_read += c;
27         }
28         return (bytes_read);
29 }
30
31
32 // Read a newline-terminated line of text from the Citadel server.
33 // Returns the string length or -1 for error.
34 int ctdl_readline(struct ctdlsession *ctdl, char *buf, int maxbytes) {
35         int len = 0;
36         int c = 0;
37
38         if (buf == NULL) {
39                 return (-1);
40         }
41
42         while (len < maxbytes) {
43                 c = read(ctdl->sock, &buf[len], 1);
44                 if (c <= 0) {
45                         syslog(LOG_DEBUG, "Socket error or zero-length read");
46                         return (-1);
47                 }
48                 if (buf[len] == '\n') {
49                         if ((len > 0) && (buf[len - 1] == '\r')) {
50                                 --len;
51                         }
52                         buf[len] = 0;
53                         // syslog(LOG_DEBUG, "[ %s", buf);
54                         return (len);
55                 }
56                 ++len;
57         }
58         // syslog(LOG_DEBUG, "[ %s", buf);
59         return (len);
60 }
61
62
63 // Read lines of text from the Citadel server until a 000 terminator is received.
64 // Implemented in terms of ctdl_readline() and is therefore transparent...
65 // Returns a newly allocated StrBuf or NULL for error.
66 StrBuf *ctdl_readtextmsg(struct ctdlsession *ctdl) {
67         char buf[1024];
68         StrBuf *sj = NewStrBuf();
69         if (!sj) {
70                 return NULL;
71         }
72
73         while (ctdl_readline(ctdl, buf, sizeof(buf)), strcmp(buf, "000")) {
74                 StrBufAppendPrintf(sj, "%s\n", buf);
75         }
76
77         return sj;
78 }
79
80
81 // Write to the Citadel server.  For now we're just wrapping write() in case we
82 // need to add anything else later.
83 ssize_t ctdl_write(struct ctdlsession *ctdl, const void *buf, size_t count) {
84         return write(ctdl->sock, buf, count);
85 }
86
87
88 // printf() type function to send data to the Citadel Server.
89 void ctdl_printf(struct ctdlsession *ctdl, const char *format, ...) {
90         va_list arg_ptr;
91         StrBuf *Buf = NewStrBuf();
92
93         va_start(arg_ptr, format);
94         StrBufVAppendPrintf(Buf, format, arg_ptr);
95         va_end(arg_ptr);
96
97         // syslog(LOG_DEBUG, "] %s", ChrPtr(Buf));
98         ctdl_write(ctdl, (char *) ChrPtr(Buf), StrLength(Buf));
99         ctdl_write(ctdl, "\n", 1);
100         FreeStrBuf(&Buf);
101 }
102
103
104 // Client side - connect to a unix domain socket
105 int uds_connectsock(char *sockpath) {
106         struct sockaddr_un addr;
107         int s;
108
109         memset(&addr, 0, sizeof(addr));
110         addr.sun_family = AF_UNIX;
111         strncpy(addr.sun_path, sockpath, sizeof addr.sun_path);
112
113         s = socket(AF_UNIX, SOCK_STREAM, 0);
114         if (s < 0) {
115                 syslog(LOG_WARNING, "Can't create socket [%s]: %s", sockpath, strerror(errno));
116                 return (-1);
117         }
118
119         if (connect(s, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
120                 syslog(LOG_WARNING, "Can't connect [%s]: %s", sockpath, strerror(errno));
121                 close(s);
122                 return (-1);
123         }
124         return s;
125 }
126
127
128 // Extract from the headers, the username and password the client is attempting to use.
129 // This could be HTTP AUTH or it could be in the cookies.
130 void extract_auth(struct http_transaction *h, char *authbuf, int authbuflen) {
131         if (authbuf == NULL) {
132                 return;
133         }
134
135         memset(authbuf, 0, authbuflen);
136
137         char *authheader = header_val(h, "Authorization");
138         if (authheader) {
139                 if (!strncasecmp(authheader, "Basic ", 6)) {
140                         safestrncpy(authbuf, &authheader[6], authbuflen);
141                         return;         // HTTP-AUTH was found -- stop here
142                 }
143         }
144
145         char *cookieheader = header_val(h, "Cookie");
146         if (cookieheader) {
147                 char *wcauth = strstr(cookieheader, "wcauth=");
148                 if (wcauth) {
149                         safestrncpy(authbuf, &cookieheader[7], authbuflen);
150                         char *semicolon = strchr(authbuf, ';');
151                         if (semicolon != NULL) {
152                                 *semicolon = 0;
153                         }
154                         if (strlen(authbuf) < 3) {      // impossibly small
155                                 authbuf[0] = 0;
156                         }
157                         return;         // Cookie auth was found -- stop here
158                 }
159         }
160         // no authorization found in headers ... this is an anonymous session
161 }
162
163
164 // Log in to the Citadel server.  Returns 0 on success or nonzero on error.
165 //
166 // 'auth' should be a base64-encoded "username:password" combination (like in http-auth)
167 //
168 // If 'resultbuf' is not NULL, it should be a buffer of at least 1024 characters,
169 // and will be filled with the result from a Citadel server command.
170 int login_to_citadel(struct ctdlsession *c, char *auth, char *resultbuf) {
171         char localbuf[1024];
172         char *buf;
173         int buflen;
174         char supplied_username[AUTH_MAX];
175         char supplied_password[AUTH_MAX];
176
177         if (resultbuf != NULL) {
178                 buf = resultbuf;
179         }
180         else {
181                 buf = localbuf;
182         }
183
184         buflen = CtdlDecodeBase64(buf, auth, strlen(auth));
185         extract_token(supplied_username, buf, 0, ':', sizeof supplied_username);
186         extract_token(supplied_password, buf, 1, ':', sizeof supplied_password);
187         syslog(LOG_DEBUG, "Supplied credentials: username=%s, password=(%d bytes)", supplied_username, (int) strlen(supplied_password));
188
189         ctdl_printf(c, "USER %s", supplied_username);
190         ctdl_readline(c, buf, 1024);
191         if (buf[0] != '3') {
192                 syslog(LOG_DEBUG, "No such user: %s", buf);
193                 return(1);      // no such user; resultbuf will explain why
194         }
195
196         ctdl_printf(c, "PASS %s", supplied_password);
197         ctdl_readline(c, buf, 1024);
198
199         if (buf[0] == '2') {
200                 extract_token(c->whoami, &buf[4], 0, '|', sizeof c->whoami);
201                 syslog(LOG_DEBUG, "Logged in as %s", c->whoami);
202
203                 // Re-encode the auth string so it contains the properly formatted username
204                 char new_auth_string[1024];
205                 snprintf(new_auth_string, sizeof(new_auth_string),  "%s:%s", c->whoami, supplied_password);
206                 CtdlEncodeBase64(c->auth, new_auth_string, strlen(new_auth_string), BASE64_NO_LINEBREAKS);
207
208                 return(0);
209         }
210
211         syslog(LOG_DEBUG, "Login failed: %s", &buf[4]);
212         return(1);              // login failed; resultbuf will explain why
213 }
214
215
216 // This is a variant of the "server connection pool" design pattern.  We go through our list
217 // of connections to Citadel Server, looking for a connection that is at once:
218 // 1. Not currently serving a WebCit transaction (is_bound)
219 // 2a. Is logged in to Citadel as the correct user, if the HTTP session is logged in; or
220 // 2b. Is NOT logged in to Citadel, if the HTTP session is not logged in.
221 // If we find a qualifying connection, we bind to it for the duration of this WebCit HTTP transaction.
222 // Otherwise, we create a new connection to Citadel Server and add it to the pool.
223 struct ctdlsession *connect_to_citadel(struct http_transaction *h) {
224         struct ctdlsession *cptr = NULL;
225         struct ctdlsession *my_session = NULL;
226         int is_new_session = 0;
227         char buf[1024];
228         char auth[AUTH_MAX];
229         int r = 0;
230
231         // Does the request carry a username and password?
232         extract_auth(h, auth, sizeof auth);
233
234         // Lock the connection pool while we claim our connection
235         pthread_mutex_lock(&cpool_mutex);
236         if (cpool != NULL) {
237                 for (cptr = cpool; ((cptr != NULL) && (my_session == NULL)); cptr = cptr->next) {
238                         if ((cptr->is_bound == 0) && (!strcmp(cptr->auth, auth))) {
239                                 my_session = cptr;
240                                 my_session->is_bound = 1;
241                         }
242                 }
243         }
244         if (my_session == NULL) {
245                 syslog(LOG_DEBUG, "No qualifying sessions , starting a new one");
246                 my_session = malloc(sizeof(struct ctdlsession));
247                 if (my_session != NULL) {
248                         memset(my_session, 0, sizeof(struct ctdlsession));
249                         is_new_session = 1;
250                         my_session->next = cpool;
251                         cpool = my_session;
252                         my_session->is_bound = 1;
253                 }
254         }
255         pthread_mutex_unlock(&cpool_mutex);
256         if (my_session == NULL) {
257                 return(NULL);                                           // Could not create a new session (yikes!)
258         }
259
260         if (my_session->sock < 3) {
261                 is_new_session = 1;
262         }
263         else {                                                          // make sure our Citadel session is still working
264                 int test_conn;
265                 test_conn = ctdl_write(my_session, HKEY("NOOP\n"));
266                 if (test_conn < 5) {
267                         syslog(LOG_DEBUG, "Citadel session is broken , must reconnect");
268                         close(my_session->sock);
269                         my_session->sock = 0;
270                         is_new_session = 1;
271                 }
272                 else {
273                         test_conn = ctdl_readline(my_session, buf, sizeof(buf));
274                         if (test_conn < 1) {
275                                 syslog(LOG_DEBUG, "Citadel session is broken , must reconnect");
276                                 close(my_session->sock);
277                                 my_session->sock = 0;
278                                 is_new_session = 1;
279                         }
280                 }
281         }
282
283         if (is_new_session) {
284                 strcpy(my_session->room, "");
285                 static char *ctdl_sock_path = NULL;
286                 if (!ctdl_sock_path) {
287                         ctdl_sock_path = malloc(PATH_MAX);
288                         snprintf(ctdl_sock_path, PATH_MAX, "%s/citadel.socket", ctdl_dir);
289                 }
290                 my_session->sock = uds_connectsock(ctdl_sock_path);
291                 ctdl_readline(my_session, buf, sizeof(buf));            // skip past the server greeting banner
292
293                 if (!IsEmptyStr(auth)) {                                // do we need to log in to Citadel?
294                         r = login_to_citadel(my_session, auth, NULL);   // FIXME figure out what happens if login failed
295                 }
296         }
297
298         ctdl_printf(my_session, "NOOP");
299         ctdl_readline(my_session, buf, sizeof(buf));
300         my_session->last_access = time(NULL);
301         ++my_session->num_requests_handled;
302         return(my_session);
303 }
304
305
306 // Release our Citadel Server connection back into the pool.
307 void disconnect_from_citadel(struct ctdlsession *ctdl) {
308         pthread_mutex_lock(&cpool_mutex);
309         ctdl->is_bound = 0;
310         pthread_mutex_unlock(&cpool_mutex);
311 }