From: Art Cancro Date: Thu, 8 Sep 2022 23:17:41 +0000 (-0400) Subject: Move *.c and *.h into server/ directory. X-Git-Tag: v958~25 X-Git-Url: https://code.citadel.org/?a=commitdiff_plain;h=6f7fc692b0121efbd826544dc837f006449a8083;p=citadel.git Move *.c and *.h into server/ directory. --- diff --git a/webcit-ng/.gitignore b/webcit-ng/.gitignore index 425df94f3..19f1cbfce 100644 --- a/webcit-ng/.gitignore +++ b/webcit-ng/.gitignore @@ -1,3 +1,4 @@ *.d *.o +*~ webcit diff --git a/webcit-ng/Makefile b/webcit-ng/Makefile index 0fc1ded6d..cda937007 100644 --- a/webcit-ng/Makefile +++ b/webcit-ng/Makefile @@ -1,24 +1,13 @@ -OBJS := http.o main.o request.o tls.o static.o tcp_sockets.o webserver.o ctdlclient.o \ - admin_functions.o room_functions.o util.o caldav_reports.o messages.o \ - ctdlfunctions.o ctdl_commands.o forum_view.o html2html.o text2html.o user_functions.o \ - floor_functions.o CFLAGS := -ggdb -Wno-format-truncation LDFLAGS := # link -webcit: $(OBJS) - gcc $(OBJS) $(LDFLAGS) -lcitadel -lpthread -lcrypto -lssl -lexpat -o webcit - -# pull in dependency info for *existing* .o files --include $(OBJS:.o=.d) - -# compile and generate dependency info -%.o: %.c - gcc -c $(CFLAGS) $*.c -o $*.o +webcit: server/*.c + gcc server/*.c $(LDFLAGS) -lcitadel -lpthread -lcrypto -lssl -lexpat -o webcit # remove compilation products clean: - rm -f webcit *.o *.d + rm -f webcit distclean: clean diff --git a/webcit-ng/admin_functions.c b/webcit-ng/admin_functions.c deleted file mode 100644 index 2c2d85f1f..000000000 --- a/webcit-ng/admin_functions.c +++ /dev/null @@ -1,105 +0,0 @@ -// Admin functions -// -// Copyright (c) 1996-2022 by the citadel.org team -// -// This program is open source software. Use, duplication, or -// disclosure are subject to the GNU General Public License v3. - -#include "webcit.h" - - -// /ctdl/a/login is called when a user is trying to log in -void try_login(struct http_transaction *h, struct ctdlsession *c) { - char buf[1024]; - char auth[AUTH_MAX]; - char username[256]; - char password[256]; - int login_success = 0; - - extract_token(username, h->request_body, 0, '|', sizeof username); - extract_token(password, h->request_body, 1, '|', sizeof password); - - snprintf(buf, sizeof buf, "%s:%s", username, password); - CtdlEncodeBase64(auth, buf, strlen(buf), BASE64_NO_LINEBREAKS); - - syslog(LOG_DEBUG, "try_login(username='%s',password=(%d bytes))", username, (int) strlen(password)); - - ctdl_printf(c, "LOUT"); // log out, in case we were logged in - ctdl_readline(c, buf, sizeof(buf)); // ignore the result - memset(c->auth, 0, AUTH_MAX); // if this connection had auth, it doesn't now. - memset(c->whoami, 0, 64); // if this connection had auth, it doesn't now. - login_success = login_to_citadel(c, auth, buf); // Now try logging in to Citadel - - JsonValue *j = NewJsonObject(HKEY("login")); // Compose a JSON object with the results - if (buf[0] == '2') { - JsonObjectAppend(j, NewJsonBool(HKEY("result"), 1)); - JsonObjectAppend(j, NewJsonPlainString(HKEY("message"), "logged in", -1)); - extract_token(username, &buf[4], 0, '|', sizeof username); // This will have the proper capitalization etc. - JsonObjectAppend(j, NewJsonPlainString(HKEY("fullname"), username, -1)); - JsonObjectAppend(j, NewJsonNumber(HKEY("axlevel"), extract_int(&buf[4], 1) )); - JsonObjectAppend(j, NewJsonNumber(HKEY("timescalled"), extract_long(&buf[4], 2) )); - JsonObjectAppend(j, NewJsonNumber(HKEY("posted"), extract_long(&buf[4], 3) )); - JsonObjectAppend(j, NewJsonNumber(HKEY("usernum"), extract_long(&buf[4], 5) )); - JsonObjectAppend(j, NewJsonNumber(HKEY("previous_login"), extract_long(&buf[4], 6) )); - } - else { - JsonObjectAppend(j, NewJsonBool(HKEY("result"), 0)); - JsonObjectAppend(j, NewJsonPlainString(HKEY("message"), &buf[4], -1)); - } - StrBuf *sj = NewStrBuf(); - SerializeJson(sj, j, 1); // '1' == free the source object - - add_response_header(h, strdup("Content-type"), strdup("application/json")); - h->response_code = 200; - h->response_string = strdup("OK"); - h->response_body_length = StrLength(sj); - h->response_body = SmashStrBuf(&sj); -} - - -// /ctdl/a/logout is called when a user is trying to log out. Don't use this as an ajax. -void logout(struct http_transaction *h, struct ctdlsession *c) { - char buf[1024]; - char auth[AUTH_MAX]; - char username[256]; - char password[256]; - int login_success = 0; - - ctdl_printf(c, "LOUT"); // log out - ctdl_readline(c, buf, sizeof(buf)); // ignore the result - strcpy(c->auth, "x"); - memset(c->whoami, 0, 64); // if this connection had auth, it doesn't now. - - http_redirect(h, "/ctdl/s/index.html"); // go back where we started :) -} - - -// /ctdl/a/whoami returns the name of the currently logged in user, or an empty string if not logged in -void whoami(struct http_transaction *h, struct ctdlsession *c) { - h->response_code = 200; - h->response_string = strdup("OK"); - add_response_header(h, strdup("Content-type"), strdup("text/plain")); - h->response_body = strdup(c->whoami); - h->response_body_length = strlen(h->response_body); -} - - -// Dispatcher for paths starting with /ctdl/a/ -void ctdl_a(struct http_transaction *h, struct ctdlsession *c) { - if (!strcasecmp(h->url, "/ctdl/a/login")) { // log in - try_login(h, c); - return; - } - - if (!strcasecmp(h->url, "/ctdl/a/logout")) { // log out - logout(h, c); - return; - } - - if (!strcasecmp(h->url, "/ctdl/a/whoami")) { // return display name of user - whoami(h, c); - return; - } - - do_404(h); // unknown -} diff --git a/webcit-ng/caldav_reports.c b/webcit-ng/caldav_reports.c deleted file mode 100644 index 9bcca613f..000000000 --- a/webcit-ng/caldav_reports.c +++ /dev/null @@ -1,270 +0,0 @@ -// -// This file contains functions which handle all of the CalDAV "REPORT" queries -// specified in RFC4791 section 7. -// -// Copyright (c) 2022 by the citadel.org team -// -// This program is open source software. Use, duplication, or -// disclosure are subject to the GNU General Public License v3. - -#include "webcit.h" - - -// A CalDAV REPORT can only be one type. This is stored in the report_type member. -enum cr_type { - cr_calendar_query, - cr_calendar_multiget, - cr_freebusy_query -}; - - -// Data type for CalDAV Report Parameters. -// As we slog our way through the XML we learn what the client is asking for -// and build up the contents of this data type. -struct cr_parms { - int tag_nesting_level; // not needed, just kept for pretty-printing - enum cr_type report_type; // which RFC4791 section 7 REPORT are we generating - StrBuf *Chardata; // XML chardata in between tags is built up here - StrBuf *Hrefs; // list of items requested by a calendar-multiget report -}; - - -// XML parser callback -void caldav_xml_start(void *data, const char *el, const char **attr) { - struct cr_parms *crp = (struct cr_parms *) data; - int i; - - // syslog(LOG_DEBUG, "CALDAV ELEMENT START: <%s> %d", el, crp->tag_nesting_level); - - for (i = 0; attr[i] != NULL; i += 2) { - syslog(LOG_DEBUG, " Attribute '%s' = '%s'", attr[i], attr[i + 1]); - } - - if (!strcasecmp(el, "urn:ietf:params:xml:ns:caldav:calendar-multiget")) { - crp->report_type = cr_calendar_multiget; - } - - else if (!strcasecmp(el, "urn:ietf:params:xml:ns:caldav:calendar-query")) { - crp->report_type = cr_calendar_query; - } - - else if (!strcasecmp(el, "urn:ietf:params:xml:ns:caldav:free-busy-query")) { - crp->report_type = cr_freebusy_query; - } - - ++crp->tag_nesting_level; -} - - -// XML parser callback -void caldav_xml_end(void *data, const char *el) { - struct cr_parms *crp = (struct cr_parms *) data; - --crp->tag_nesting_level; - - if (crp->Chardata != NULL) { - // syslog(LOG_DEBUG, "CALDAV CHARDATA : %s", ChrPtr(crp->Chardata)); - } - // syslog(LOG_DEBUG, "CALDAV ELEMENT END : <%s> %d", el, crp->tag_nesting_level); - - if ((!strcasecmp(el, "DAV::href")) || (!strcasecmp(el, "DAV:href"))) { - if (crp->Hrefs == NULL) { // append crp->Chardata to crp->Hrefs - crp->Hrefs = NewStrBuf(); - } - else { - StrBufAppendBufPlain(crp->Hrefs, HKEY("|"), 0); - } - StrBufAppendBuf(crp->Hrefs, crp->Chardata, 0); - } - - if (crp->Chardata != NULL) { // Tag is closed; chardata is now out of scope. - FreeStrBuf(&crp->Chardata); // Free the buffer. - crp->Chardata = NULL; - } -} - - -// XML parser callback -void caldav_xml_chardata(void *data, const XML_Char * s, int len) { - struct cr_parms *crp = (struct cr_parms *) data; - - if (crp->Chardata == NULL) { - crp->Chardata = NewStrBuf(); - } - - StrBufAppendBufPlain(crp->Chardata, s, len, 0); - - return; -} - - -// Called by caldav_response() to fetch a message (by number) in the current room, -// and return only the icalendar data as a StrBuf. Returns NULL if not found. -// -// NOTE: this function expects that "MSGP text/calendar" was issued at the beginning -// of a REPORT operation to set our preferred MIME type to calendar data. -StrBuf *fetch_ical(struct ctdlsession * c, long msgnum) { - char buf[1024]; - StrBuf *Buf = NULL; - - ctdl_printf(c, "MSG4 %ld", msgnum); - ctdl_readline(c, buf, sizeof(buf)); - if (buf[0] != '1') { - return NULL; - } - - while (ctdl_readline(c, buf, sizeof(buf)), strcmp(buf, "000")) { - if (Buf != NULL) { // already in body - StrBufAppendPrintf(Buf, "%s\n", buf); - } - else if (IsEmptyStr(buf)) { // beginning of body - Buf = NewStrBuf(); - } - } - - return Buf; - -// webcit[13039]: msgn=53CE87AF-00392161@uncensored.citadel.org -// webcit[13039]: path=IGnatius T Foobar -// webcit[13039]: time=1208008800 -// webcit[13039]: from=IGnatius T Foobar -// webcit[13039]: room=0000000001.Calendar -// webcit[13039]: node=uncnsrd -// webcit[13039]: hnod=Uncensored -// webcit[13039]: exti=040000008200E00074C5B7101A82E0080000000080C728F83E84C801000000000000000010000000E857E0DC57F53947ADF0BB91EE3A502F -// webcit[13039]: subj==?UTF-8?B?V2VzbGV5J3MgYmlydGhkYXkgcGFydHk= -// webcit[13039]: ?= -// webcit[13039]: part=||1||text/calendar|1127|| -// webcit[13039]: text -// webcit[13039]: Content-type: text/calendar -// webcit[13039]: Content-length: 1127 -// webcit[13039]: Content-transfer-encoding: 7bit -// webcit[13039]: X-Citadel-MSG4-Partnum: 1 -// webcit[13039]: -// webcit[13039]: BEGIN:VCALENDAR -} - - -// Called by caldav_report() to output a single item. -// Our policy is to throw away the list of properties the client asked for, and just send everything. -void caldav_response(struct http_transaction *h, struct ctdlsession *c, StrBuf * ReportOut, StrBuf * ThisHref) { - long msgnum; - StrBuf *Caldata = NULL; - char *euid; - - euid = strrchr(ChrPtr(ThisHref), '/'); - if (euid != NULL) { - ++euid; - } - else { - euid = (char *) ChrPtr(ThisHref); - } - - char *unescaped_euid = strdup(euid); - if (!unescaped_euid) { - return; - } - unescape_input(unescaped_euid); - - StrBufAppendPrintf(ReportOut, ""); - StrBufAppendPrintf(ReportOut, ""); - StrBufXMLEscAppend(ReportOut, ThisHref, NULL, 0, 0); - StrBufAppendPrintf(ReportOut, ""); - StrBufAppendPrintf(ReportOut, ""); - - msgnum = locate_message_by_uid(c, unescaped_euid); - free(unescaped_euid); - if (msgnum > 0) { - Caldata = fetch_ical(c, msgnum); - } - - if (Caldata != NULL) { - // syslog(LOG_DEBUG, "caldav_response(%s) 200 OK", ChrPtr(ThisHref)); - StrBufAppendPrintf(ReportOut, ""); - StrBufAppendPrintf(ReportOut, "HTTP/1.1 200 OK"); - StrBufAppendPrintf(ReportOut, ""); - StrBufAppendPrintf(ReportOut, ""); - StrBufAppendPrintf(ReportOut, ""); - StrBufAppendPrintf(ReportOut, "%ld", msgnum); - StrBufAppendPrintf(ReportOut, ""); - StrBufAppendPrintf(ReportOut, ""); - StrBufXMLEscAppend(ReportOut, Caldata, NULL, 0, 0); - StrBufAppendPrintf(ReportOut, ""); - StrBufAppendPrintf(ReportOut, ""); - FreeStrBuf(&Caldata); - Caldata = NULL; - } - else { - // syslog(LOG_DEBUG, "caldav_response(%s) 404 not found", ChrPtr(ThisHref)); - StrBufAppendPrintf(ReportOut, ""); - StrBufAppendPrintf(ReportOut, "HTTP/1.1 404 not found"); - StrBufAppendPrintf(ReportOut, ""); - } - - StrBufAppendPrintf(ReportOut, ""); - StrBufAppendPrintf(ReportOut, ""); -} - - -// Called by report_the_room_itself() in room_functions.c when a CalDAV REPORT method -// is requested on a calendar room. We fire up an XML Parser to decode the request and -// hopefully produce the correct output. -void caldav_report(struct http_transaction *h, struct ctdlsession *c) { - struct cr_parms crp; - char buf[1024]; - - memset(&crp, 0, sizeof(struct cr_parms)); - - XML_Parser xp = XML_ParserCreateNS("UTF-8", ':'); - if (xp == NULL) { - syslog(LOG_INFO, "Cannot create XML parser!"); - do_404(h); - return; - } - - XML_SetElementHandler(xp, caldav_xml_start, caldav_xml_end); - XML_SetCharacterDataHandler(xp, caldav_xml_chardata); - XML_SetUserData(xp, &crp); - XML_SetDefaultHandler(xp, NULL); // Disable internal entity expansion to prevent "billion laughs attack" - XML_Parse(xp, h->request_body, h->request_body_length, 1); - XML_ParserFree(xp); - - if (crp.Chardata != NULL) { // Discard any trailing chardata ... normally nothing here - FreeStrBuf(&crp.Chardata); - crp.Chardata = NULL; - } - - // We're going to make a lot of MSG4 calls, and the preferred MIME type we want is "text/calendar". - // The iCalendar standard is mature now, and we are no longer interested in text/x-vcal or application/ics. - ctdl_printf(c, "MSGP text/calendar"); - ctdl_readline(c, buf, sizeof buf); - - // Now begin the REPORT. - syslog(LOG_DEBUG, "CalDAV REPORT type is: %d", crp.report_type); - StrBuf *ReportOut = NewStrBuf(); - StrBufAppendPrintf(ReportOut, - "" - "" - ); - - if (crp.Hrefs != NULL) { // Output all qualifying calendar items! - StrBuf *ThisHref = NewStrBuf(); - const char *pvset = NULL; - while (StrBufExtract_NextToken(ThisHref, crp.Hrefs, &pvset, '|') >= 0) { - caldav_response(h, c, ReportOut, ThisHref); - } - FreeStrBuf(&ThisHref); - FreeStrBuf(&crp.Hrefs); - crp.Hrefs = NULL; - } - - StrBufAppendPrintf(ReportOut, "\n"); // End the REPORT. - - add_response_header(h, strdup("Content-type"), strdup("text/xml")); - h->response_code = 207; - h->response_string = strdup("Multi-Status"); - h->response_body_length = StrLength(ReportOut); - h->response_body = SmashStrBuf(&ReportOut); -} diff --git a/webcit-ng/ctdl_commands.c b/webcit-ng/ctdl_commands.c deleted file mode 100644 index 6420a8750..000000000 --- a/webcit-ng/ctdl_commands.c +++ /dev/null @@ -1,94 +0,0 @@ -// -// Copyright (c) 1996-2022 by the citadel.org team -// -// This program is open source software. Use, duplication, or -// disclosure are subject to the GNU General Public License v3. - -#include "webcit.h" - - -// /ctdl/c/info returns a JSON representation of the output of an INFO command. -void serv_info(struct http_transaction *h, struct ctdlsession *c) { - char buf[1024]; - - ctdl_printf(c, "INFO"); - ctdl_readline(c, buf, sizeof(buf)); - if (buf[0] != '1') { - do_502(h); - return; - } - - JsonValue *j = NewJsonObject(HKEY("serv_info")); - int i = 0; - while (ctdl_readline(c, buf, sizeof(buf)), strcmp(buf, "000")) - switch (i++) { - case 0: - JsonObjectAppend(j, NewJsonNumber(HKEY("serv_pid"), atol(buf))); - break; - case 1: - JsonObjectAppend(j, NewJsonPlainString(HKEY("serv_nodename"), buf, -1)); - break; - case 2: - JsonObjectAppend(j, NewJsonPlainString(HKEY("serv_humannode"), buf, -1)); - break; - case 3: - JsonObjectAppend(j, NewJsonPlainString(HKEY("serv_fqdn"), buf, -1)); - break; - case 4: - JsonObjectAppend(j, NewJsonPlainString(HKEY("serv_software"), buf, -1)); - break; - case 5: - JsonObjectAppend(j, NewJsonNumber(HKEY("serv_rev_level"), atol(buf))); - break; - case 6: - JsonObjectAppend(j, NewJsonPlainString(HKEY("serv_bbs_city"), buf, -1)); - break; - case 7: - JsonObjectAppend(j, NewJsonPlainString(HKEY("serv_sysadm"), buf, -1)); - break; - case 14: - JsonObjectAppend(j, NewJsonBool(HKEY("serv_supports_ldap"), atoi(buf))); - break; - case 15: - JsonObjectAppend(j, NewJsonBool(HKEY("serv_newuser_disabled"), atoi(buf))); - break; - case 16: - JsonObjectAppend(j, NewJsonPlainString(HKEY("serv_default_cal_zone"), buf, -1)); - break; - case 20: - JsonObjectAppend(j, NewJsonBool(HKEY("serv_supports_sieve"), atoi(buf))); - break; - case 21: - JsonObjectAppend(j, NewJsonBool(HKEY("serv_fulltext_enabled"), atoi(buf))); - break; - case 22: - JsonObjectAppend(j, NewJsonPlainString(HKEY("serv_svn_revision"), buf, -1)); - break; - case 23: - JsonObjectAppend(j, NewJsonBool(HKEY("serv_supports_openid"), atoi(buf))); - break; - case 24: - JsonObjectAppend(j, NewJsonBool(HKEY("serv_supports_guest"), atoi(buf))); - break; - } - - StrBuf *sj = NewStrBuf(); - SerializeJson(sj, j, 1); // '1' == free the source array - - add_response_header(h, strdup("Content-type"), strdup("application/json")); - h->response_code = 200; - h->response_string = strdup("OK"); - h->response_body_length = StrLength(sj); - h->response_body = SmashStrBuf(&sj); -} - - -// Dispatcher for paths starting with /ctdl/c/ -void ctdl_c(struct http_transaction *h, struct ctdlsession *c) { - if (!strcasecmp(h->url, "/ctdl/c/info")) { - serv_info(h, c); - } - else { - do_404(h); - } -} diff --git a/webcit-ng/ctdlclient.c b/webcit-ng/ctdlclient.c deleted file mode 100644 index fa35595e6..000000000 --- a/webcit-ng/ctdlclient.c +++ /dev/null @@ -1,311 +0,0 @@ -// Functions that handle communication with a Citadel Server -// -// Copyright (c) 1987-2022 by the citadel.org team -// -// This program is open source software. Use, duplication, or -// disclosure are subject to the GNU General Public License v3. - -#include "webcit.h" - -struct ctdlsession *cpool = NULL; // linked list of connections to the Citadel server -pthread_mutex_t cpool_mutex = PTHREAD_MUTEX_INITIALIZER; // Lock it before modifying - - -// Read a specific number of bytes of binary data from the Citadel server. -// Returns the number of bytes read or -1 for error. -int ctdl_read_binary(struct ctdlsession *ctdl, char *buf, int bytes_requested) { - int bytes_read = 0; - int c = 0; - - while (bytes_read < bytes_requested) { - c = read(ctdl->sock, &buf[bytes_read], bytes_requested-bytes_read); - if (c <= 0) { - syslog(LOG_DEBUG, "Socket error or zero-length read"); - return (-1); - } - bytes_read += c; - } - return (bytes_read); -} - - -// Read a newline-terminated line of text from the Citadel server. -// Returns the string length or -1 for error. -int ctdl_readline(struct ctdlsession *ctdl, char *buf, int maxbytes) { - int len = 0; - int c = 0; - - if (buf == NULL) { - return (-1); - } - - while (len < maxbytes) { - c = read(ctdl->sock, &buf[len], 1); - if (c <= 0) { - syslog(LOG_DEBUG, "Socket error or zero-length read"); - return (-1); - } - if (buf[len] == '\n') { - if ((len > 0) && (buf[len - 1] == '\r')) { - --len; - } - buf[len] = 0; - // syslog(LOG_DEBUG, "\033[32m[ %s\033[0m", buf); - return (len); - } - ++len; - } - // syslog(LOG_DEBUG, "\033[32m[ %s\033[0m", buf); - return (len); -} - - -// Read lines of text from the Citadel server until a 000 terminator is received. -// Implemented in terms of ctdl_readline() and is therefore transparent... -// Returns a newly allocated StrBuf or NULL for error. -StrBuf *ctdl_readtextmsg(struct ctdlsession *ctdl) { - char buf[1024]; - StrBuf *sj = NewStrBuf(); - if (!sj) { - return NULL; - } - - while (ctdl_readline(ctdl, buf, sizeof(buf)), strcmp(buf, "000")) { - StrBufAppendPrintf(sj, "%s\n", buf); - } - - return sj; -} - - -// Write to the Citadel server. For now we're just wrapping write() in case we -// need to add anything else later. -ssize_t ctdl_write(struct ctdlsession *ctdl, const void *buf, size_t count) { - return write(ctdl->sock, buf, count); -} - - -// printf() type function to send data to the Citadel Server. -void ctdl_printf(struct ctdlsession *ctdl, const char *format, ...) { - va_list arg_ptr; - StrBuf *Buf = NewStrBuf(); - - va_start(arg_ptr, format); - StrBufVAppendPrintf(Buf, format, arg_ptr); - va_end(arg_ptr); - - // syslog(LOG_DEBUG, "\033[32m] %s\033[0m", ChrPtr(Buf)); - ctdl_write(ctdl, (char *) ChrPtr(Buf), StrLength(Buf)); - ctdl_write(ctdl, "\n", 1); - FreeStrBuf(&Buf); -} - - -// Client side - connect to a unix domain socket -int uds_connectsock(char *sockpath) { - struct sockaddr_un addr; - int s; - - memset(&addr, 0, sizeof(addr)); - addr.sun_family = AF_UNIX; - strncpy(addr.sun_path, sockpath, sizeof addr.sun_path); - - s = socket(AF_UNIX, SOCK_STREAM, 0); - if (s < 0) { - syslog(LOG_WARNING, "Can't create socket [%s]: %s", sockpath, strerror(errno)); - return (-1); - } - - if (connect(s, (struct sockaddr *) &addr, sizeof(addr)) < 0) { - syslog(LOG_WARNING, "Can't connect [%s]: %s", sockpath, strerror(errno)); - close(s); - return (-1); - } - return s; -} - - -// Extract from the headers, the username and password the client is attempting to use. -// This could be HTTP AUTH or it could be in the cookies. -void extract_auth(struct http_transaction *h, char *authbuf, int authbuflen) { - if (authbuf == NULL) { - return; - } - - memset(authbuf, 0, authbuflen); - - char *authheader = header_val(h, "Authorization"); - if (authheader) { - if (!strncasecmp(authheader, "Basic ", 6)) { - safestrncpy(authbuf, &authheader[6], authbuflen); - return; // HTTP-AUTH was found -- stop here - } - } - - char *cookieheader = header_val(h, "Cookie"); - if (cookieheader) { - char *wcauth = strstr(cookieheader, "wcauth="); - if (wcauth) { - safestrncpy(authbuf, &cookieheader[7], authbuflen); - char *semicolon = strchr(authbuf, ';'); - if (semicolon != NULL) { - *semicolon = 0; - } - if (strlen(authbuf) < 3) { // impossibly small - authbuf[0] = 0; - } - return; // Cookie auth was found -- stop here - } - } - // no authorization found in headers ... this is an anonymous session -} - - -// Log in to the Citadel server. Returns 0 on success or nonzero on error. -// -// 'auth' should be a base64-encoded "username:password" combination (like in http-auth) -// -// If 'resultbuf' is not NULL, it should be a buffer of at least 1024 characters, -// and will be filled with the result from a Citadel server command. -int login_to_citadel(struct ctdlsession *c, char *auth, char *resultbuf) { - char localbuf[1024]; - char *buf; - int buflen; - char supplied_username[AUTH_MAX]; - char supplied_password[AUTH_MAX]; - - if (resultbuf != NULL) { - buf = resultbuf; - } - else { - buf = localbuf; - } - - buflen = CtdlDecodeBase64(buf, auth, strlen(auth)); - extract_token(supplied_username, buf, 0, ':', sizeof supplied_username); - extract_token(supplied_password, buf, 1, ':', sizeof supplied_password); - syslog(LOG_DEBUG, "Supplied credentials: username=%s, password=(%d bytes)", supplied_username, (int) strlen(supplied_password)); - - ctdl_printf(c, "USER %s", supplied_username); - ctdl_readline(c, buf, 1024); - if (buf[0] != '3') { - syslog(LOG_DEBUG, "No such user: %s", buf); - return(1); // no such user; resultbuf will explain why - } - - ctdl_printf(c, "PASS %s", supplied_password); - ctdl_readline(c, buf, 1024); - - if (buf[0] == '2') { - extract_token(c->whoami, &buf[4], 0, '|', sizeof c->whoami); - syslog(LOG_DEBUG, "Logged in as %s", c->whoami); - - // Re-encode the auth string so it contains the properly formatted username - char new_auth_string[1024]; - snprintf(new_auth_string, sizeof(new_auth_string), "%s:%s", c->whoami, supplied_password); - CtdlEncodeBase64(c->auth, new_auth_string, strlen(new_auth_string), BASE64_NO_LINEBREAKS); - - return(0); - } - - syslog(LOG_DEBUG, "Login failed: %s", &buf[4]); - return(1); // login failed; resultbuf will explain why -} - - -// This is a variant of the "server connection pool" design pattern. We go through our list -// of connections to Citadel Server, looking for a connection that is at once: -// 1. Not currently serving a WebCit transaction (is_bound) -// 2a. Is logged in to Citadel as the correct user, if the HTTP session is logged in; or -// 2b. Is NOT logged in to Citadel, if the HTTP session is not logged in. -// If we find a qualifying connection, we bind to it for the duration of this WebCit HTTP transaction. -// Otherwise, we create a new connection to Citadel Server and add it to the pool. -struct ctdlsession *connect_to_citadel(struct http_transaction *h) { - struct ctdlsession *cptr = NULL; - struct ctdlsession *my_session = NULL; - int is_new_session = 0; - char buf[1024]; - char auth[AUTH_MAX]; - int r = 0; - - // Does the request carry a username and password? - extract_auth(h, auth, sizeof auth); - - // Lock the connection pool while we claim our connection - pthread_mutex_lock(&cpool_mutex); - if (cpool != NULL) { - for (cptr = cpool; ((cptr != NULL) && (my_session == NULL)); cptr = cptr->next) { - if ((cptr->is_bound == 0) && (!strcmp(cptr->auth, auth))) { - my_session = cptr; - my_session->is_bound = 1; - } - } - } - if (my_session == NULL) { - syslog(LOG_DEBUG, "No qualifying sessions , starting a new one"); - my_session = malloc(sizeof(struct ctdlsession)); - if (my_session != NULL) { - memset(my_session, 0, sizeof(struct ctdlsession)); - is_new_session = 1; - my_session->next = cpool; - cpool = my_session; - my_session->is_bound = 1; - } - } - pthread_mutex_unlock(&cpool_mutex); - if (my_session == NULL) { - return(NULL); // Could not create a new session (yikes!) - } - - if (my_session->sock < 3) { - is_new_session = 1; - } - else { // make sure our Citadel session is still working - int test_conn; - test_conn = ctdl_write(my_session, HKEY("NOOP\n")); - if (test_conn < 5) { - syslog(LOG_DEBUG, "Citadel session is broken , must reconnect"); - close(my_session->sock); - my_session->sock = 0; - is_new_session = 1; - } - else { - test_conn = ctdl_readline(my_session, buf, sizeof(buf)); - if (test_conn < 1) { - syslog(LOG_DEBUG, "Citadel session is broken , must reconnect"); - close(my_session->sock); - my_session->sock = 0; - is_new_session = 1; - } - } - } - - if (is_new_session) { - strcpy(my_session->room, ""); - static char *ctdl_sock_path = NULL; - if (!ctdl_sock_path) { - ctdl_sock_path = malloc(PATH_MAX); - snprintf(ctdl_sock_path, PATH_MAX, "%s/citadel.socket", ctdl_dir); - } - my_session->sock = uds_connectsock(ctdl_sock_path); - ctdl_readline(my_session, buf, sizeof(buf)); // skip past the server greeting banner - - if (!IsEmptyStr(auth)) { // do we need to log in to Citadel? - r = login_to_citadel(my_session, auth, NULL); // FIXME figure out what happens if login failed - } - } - - ctdl_printf(my_session, "NOOP"); - ctdl_readline(my_session, buf, sizeof(buf)); - my_session->last_access = time(NULL); - ++my_session->num_requests_handled; - return(my_session); -} - - -// Release our Citadel Server connection back into the pool. -void disconnect_from_citadel(struct ctdlsession *ctdl) { - pthread_mutex_lock(&cpool_mutex); - ctdl->is_bound = 0; - pthread_mutex_unlock(&cpool_mutex); -} diff --git a/webcit-ng/ctdlfunctions.c b/webcit-ng/ctdlfunctions.c deleted file mode 100644 index ba25fd637..000000000 --- a/webcit-ng/ctdlfunctions.c +++ /dev/null @@ -1,36 +0,0 @@ -// -// These utility functions loosely make up a Citadel protocol client library. -// -// Copyright (c) 2016-2022 by the citadel.org team -// -// This program is open source software. Use, duplication, or -// disclosure are subject to the GNU General Public License v3. - -#include "webcit.h" - -// Delete one or more messages from the connected Citadel server. -// This function expects the session to already be "in" the room from which the messages will be deleted. -void ctdl_delete_msgs(struct ctdlsession *c, long *msgnums, int num_msgs) { - int i = 0; - char buf[1024]; - - if ((c == NULL) || (msgnums == NULL) || (num_msgs < 1)) { - return; - } - - i = 0; - strcpy(buf, "DELE "); - do { - sprintf(&buf[strlen(buf)], "%ld", msgnums[i]); - if ((((i + 1) % 50) == 0) || (i == num_msgs - 1)) // delete up to 50 messages with one server command - { - syslog(LOG_DEBUG, "%s", buf); - ctdl_printf(c, "%s", buf); - ctdl_readline(c, buf, sizeof(buf)); - syslog(LOG_DEBUG, "%s", buf); - } - else { - strcat(buf, ","); - } - } while (++i < num_msgs); -} diff --git a/webcit-ng/floor_functions.c b/webcit-ng/floor_functions.c deleted file mode 100644 index ccb632668..000000000 --- a/webcit-ng/floor_functions.c +++ /dev/null @@ -1,56 +0,0 @@ -// -// Floor functions -// -// Copyright (c) 1996-2022 by the citadel.org team -// -// This program is open source software. Use, duplication, or -// disclosure are subject to the GNU General Public License v3. - -#include "webcit.h" - - -// Dispatcher for "/ctdl/f" and "/ctdl/f/" for the floor list -void floor_list(struct http_transaction *h, struct ctdlsession *c) { - char buf[1024]; - char floorname[1024]; - - ctdl_printf(c, "LFLR"); - ctdl_readline(c, buf, sizeof(buf)); - if (buf[0] != '1') { - do_502(h); - return; - } - - JsonValue *j = NewJsonArray(HKEY("lflr")); - while (ctdl_readline(c, buf, sizeof(buf)), strcmp(buf, "000")) { - - // 0 |1 |2 - // num|name|refcount - JsonValue *jr = NewJsonObject(HKEY("floor")); - - extract_token(floorname, buf, 1, '|', sizeof floorname); - JsonObjectAppend(jr, NewJsonPlainString(HKEY("name"), floorname, -1)); - - JsonObjectAppend(jr, NewJsonNumber(HKEY("num"), extract_int(buf, 0))); - JsonObjectAppend(jr, NewJsonNumber(HKEY("refcount"), extract_int(buf, 2))); - - JsonArrayAppend(j, jr); // add the room to the array - } - - StrBuf *sj = NewStrBuf(); - SerializeJson(sj, j, 1); // '1' == free the source array - - add_response_header(h, strdup("Content-type"), strdup("application/json")); - h->response_code = 200; - h->response_string = strdup("OK"); - h->response_body_length = StrLength(sj); - h->response_body = SmashStrBuf(&sj); -} - - -// Dispatcher for paths starting with /ctdl/f/ -// (This is a stub ... we will need to add more functions when we can do more than just a floor list) -void ctdl_f(struct http_transaction *h, struct ctdlsession *c) { - floor_list(h, c); - return; -} diff --git a/webcit-ng/forum_view.c b/webcit-ng/forum_view.c deleted file mode 100644 index 90ef33746..000000000 --- a/webcit-ng/forum_view.c +++ /dev/null @@ -1,148 +0,0 @@ -// The code in here feeds messages out as JSON to the client browser. It is currently being used -// for the forum view, but as we implement other views we'll probably reuse a lot of what's here. -// -// Copyright (c) 1996-2022 by the citadel.org team -// -// This program is open source software. Use, duplication, or -// disclosure are subject to the GNU General Public License v3. - -#include "webcit.h" - -// Commands we need to send to Citadel Server before we begin rendering forum view. -// These are common to flat and threaded views. -void setup_for_forum_view(struct ctdlsession *c) { - char buf[1024]; - ctdl_printf(c, "MSGP text/html|text/plain"); // Declare the MIME types we know how to render - ctdl_readline(c, buf, sizeof(buf)); // Ignore the response - ctdl_printf(c, "MSGP dont_decode"); // Tell the server we will decode base64/etc client-side - ctdl_readline(c, buf, sizeof(buf)); // Ignore the response -} - - -// Fetch a single message and return it in JSON format for client-side rendering -void json_render_one_message(struct http_transaction *h, struct ctdlsession *c, long msgnum) { - StrBuf *raw_msg = NULL; - StrBuf *sanitized_msg = NULL; - char buf[1024]; - char content_transfer_encoding[1024] = { 0 }; - char content_type[1024] = { 0 }; - char datetime[128] = { 0 }; - char author[1024] = { 0 }; - char emailaddr[1024] = { 0 }; - int message_originated_locally = 0; - - setup_for_forum_view(c); - - ctdl_printf(c, "MSG4 %ld", msgnum); - ctdl_readline(c, buf, sizeof(buf)); - if (buf[0] != '1') { - do_404(h); - return; - } - - JsonValue *j = NewJsonObject(HKEY("message")); - JsonObjectAppend(j, NewJsonNumber(HKEY("msgnum"), msgnum)); - - while ((ctdl_readline(c, buf, sizeof(buf)) >= 0) && (strcmp(buf, "text")) && (strcmp(buf, "000"))) { - utf8ify_rfc822_string(&buf[5]); - - // citadel header parsing here - if (!strncasecmp(buf, "from=", 5)) { - safestrncpy(author, &buf[5], sizeof author); - } - else if (!strncasecmp(buf, "rfca=", 5)) { - safestrncpy(emailaddr, &buf[5], sizeof emailaddr); - } - else if (!strncasecmp(buf, "time=", 5)) { - JsonObjectAppend(j, NewJsonNumber(HKEY("time"), atol(&buf[5]))); - } - else if (!strncasecmp(buf, "locl=", 5)) { - message_originated_locally = 1; - } - else if (!strncasecmp(buf, "subj=", 5)) { - JsonObjectAppend(j, NewJsonPlainString(HKEY("subj"), &buf[5], -1)); - } - else if (!strncasecmp(buf, "msgn=", 5)) { - JsonObjectAppend(j, NewJsonPlainString(HKEY("msgn"), &buf[5], -1)); - } - else if (!strncasecmp(buf, "wefw=", 5)) { - JsonObjectAppend(j, NewJsonPlainString(HKEY("wefw"), &buf[5], -1)); - } - } - - if (message_originated_locally) { - JsonObjectAppend(j, NewJsonPlainString(HKEY("from"), author, -1)); - } - else { - JsonObjectAppend(j, NewJsonPlainString(HKEY("from"), emailaddr, -1)); // FIXME do compound address string - } - - if (!strcmp(buf, "text")) { - while ((ctdl_readline(c, buf, sizeof(buf)) >= 0) && (strcmp(buf, "")) && (strcmp(buf, "000"))) { - // rfc822 header parsing here - if (!strncasecmp(buf, "Content-transfer-encoding:", 26)) { - strcpy(content_transfer_encoding, &buf[26]); - striplt(content_transfer_encoding); - } - if (!strncasecmp(buf, "Content-type:", 13)) { - strcpy(content_type, &buf[13]); - striplt(content_type); - } - } - if (!strcmp(buf, "000")) { // if we have an empty message, don't try to read further - raw_msg = NULL; - } - else { - raw_msg = ctdl_readtextmsg(c); - } - } - else { - raw_msg = NULL; - } - - if (raw_msg) { - // These are the encodings we know how to handle. Decode in-place. - - if (!strcasecmp(content_transfer_encoding, "base64")) { - StrBufDecodeBase64(raw_msg); - } - if (!strcasecmp(content_transfer_encoding, "quoted-printable")) { - StrBufDecodeQP(raw_msg); - } - - // At this point, raw_msg contains the decoded message. - // Now run through the renderers we have available. - - if (!strncasecmp(content_type, "text/html", 9)) { - sanitized_msg = html2html("UTF-8", 0, c->room, msgnum, raw_msg); - } - else if (!strncasecmp(content_type, "text/plain", 10)) { - sanitized_msg = text2html("UTF-8", 0, c->room, msgnum, raw_msg); - } - else if (!strncasecmp(content_type, "text/x-citadel-variformat", 25)) { - sanitized_msg = variformat2html(raw_msg); - } - else { - sanitized_msg = NewStrBufPlain(HKEY("No renderer for this content type
")); - syslog(LOG_WARNING, "forum_view: no renderer for content type %s", content_type); - } - FreeStrBuf(&raw_msg); - - // If sanitized_msg is not NULL, we have rendered the message and can output it. - if (sanitized_msg) { - JsonObjectAppend(j, NewJsonString(HKEY("text"), sanitized_msg, NEWJSONSTRING_SMASHBUF)); - } - else { - syslog(LOG_WARNING, "forum_view: message %ld of content type %s converted to NULL", msgnum, content_type); - } - } - - StrBuf *sj = NewStrBuf(); - SerializeJson(sj, j, 1); // '1' == free the source object - - add_response_header(h, strdup("Content-type"), strdup("application/json")); - h->response_code = 200; - h->response_string = strdup("OK"); - h->response_body_length = StrLength(sj); - h->response_body = SmashStrBuf(&sj); -} diff --git a/webcit-ng/html2html.c b/webcit-ng/html2html.c deleted file mode 100644 index b6dadc35e..000000000 --- a/webcit-ng/html2html.c +++ /dev/null @@ -1,635 +0,0 @@ -// -// Output an HTML message, modifying it slightly to make sure it plays nice -// with the rest of our web framework. -// -// Copyright (c) 2005-2022 by the citadel.org team -// -// This program is open source software. Use, duplication, or -// disclosure are subject to the GNU General Public License v3. - -#include "webcit.h" - - -// Strip surrounding single or double quotes from a string. -void stripquotes(char *s) { - int len; - - if (!s) - return; - - len = strlen(s); - if (len < 2) - return; - - if (((s[0] == '\"') && (s[len - 1] == '\"')) || ((s[0] == '\'') && (s[len - 1] == '\''))) { - s[len - 1] = 0; - strcpy(s, &s[1]); - } -} - - -// Check to see if a META tag has overridden the declared MIME character set. -// -// charset Character set name (left unchanged if we don't do anything) -// meta_http_equiv Content of the "http-equiv" portion of the META tag -// meta_content Content of the "content" portion of the META tag -void extract_charset_from_meta(char *charset, char *meta_http_equiv, char *meta_content) { - char *ptr; - char buf[64]; - - if (!charset) - return; - if (!meta_http_equiv) - return; - if (!meta_content) - return; - - if (strcasecmp(meta_http_equiv, "Content-type")) - return; - - ptr = strchr(meta_content, ';'); - if (!ptr) - return; - - safestrncpy(buf, ++ptr, sizeof buf); - striplt(buf); - if (!strncasecmp(buf, "charset=", 8)) { - strcpy(charset, &buf[8]); - - // The brain-damaged webmail program in Microsoft Exchange declares - // a charset of "unicode" when they really mean "UTF-8". GNU iconv - // treats "unicode" as an alias for "UTF-16" so we have to manually - // fix this here, otherwise messages generated in Exchange webmail - // show up as a big pile of weird characters. - if (!strcasecmp(charset, "unicode")) { - strcpy(charset, "UTF-8"); - } - - // Remove wandering punctuation - if ((ptr = strchr(charset, '\"'))) - *ptr = 0; - striplt(charset); - } -} - - -// Sanitize and enhance an HTML message for display. -// Also convert weird character sets to UTF-8 if necessary. -// Also fixup img src="cid:..." type inline images to fetch the image -StrBuf *html2html(const char *supplied_charset, int treat_as_wiki, char *roomname, long msgnum, StrBuf *Source) { - char buf[SIZ]; - char *msg; - char *ptr; - char *msgstart; - char *msgend; - StrBuf *converted_msg; - int buffer_length = 1; - int line_length = 0; - int content_length = 0; - char new_window[SIZ]; - int brak = 0; - int alevel = 0; - int scriptlevel = 0; - int script_start_pos = (-1); - int i; - int linklen; - char charset[128]; - StrBuf *BodyArea = NULL; - - iconv_t ic = (iconv_t) (-1); - char *ibuf; // Buffer of characters to be converted - char *obuf; // Buffer for converted characters - size_t ibuflen; // Length of input buffer - size_t obuflen; // Length of output buffer - char *osav; // Saved pointer to output buffer - - StrBuf *Target = NewStrBuf(); - if (Target == NULL) { - return (NULL); - } - - safestrncpy(charset, supplied_charset, sizeof charset); - sprintf(new_window, "'); - if ((meta_end != NULL) && (meta_end <= msgend)) { - meta_length = meta_end - meta_start + 1; - meta = malloc(meta_length + 1); - safestrncpy(meta, meta_start, meta_length); - meta[meta_length] = 0; - striplt(meta); - if (!strncasecmp(meta, "HTTP-EQUIV=", 11)) { - meta_http_equiv = strdup(&meta[11]); - spaceptr = strchr(meta_http_equiv, ' '); - if (spaceptr != NULL) { - *spaceptr = 0; - meta_content = strdup(++spaceptr); - if (!strncasecmp(meta_content, "content=", 8)) { - strcpy(meta_content, &meta_content[8]); - stripquotes(meta_http_equiv); - stripquotes(meta_content); - extract_charset_from_meta(charset, meta_http_equiv, meta_content); - } - free(meta_content); - } - free(meta_http_equiv); - } - free(meta); - } - } - - // Any of these tags cause everything up to and including - // the tag to be removed. - if ((!strncasecmp(ptr, "HTML", 4)) - || (!strncasecmp(ptr, "HEAD", 4)) - || (!strncasecmp(ptr, "/HEAD", 5)) - || (!strncasecmp(ptr, "BODY", 4))) { - char *pBody = NULL; - - if (!strncasecmp(ptr, "BODY", 4)) { - pBody = ptr; - } - ptr = strchr(ptr, '>'); - if ((ptr == NULL) || (ptr >= msgend)) - break; - if ((pBody != NULL) && (ptr - pBody > 4)) { - char *src; - char *cid_start, *cid_end; - - *ptr = '\0'; - pBody += 4; - while ((isspace(*pBody)) && (pBody < ptr)) - pBody++; - BodyArea = NewStrBufPlain(NULL, ptr - pBody); - - if (pBody < ptr) { - src = strstr(pBody, "cid:"); - if (src) { - cid_start = src + 4; - cid_end = cid_start; - while ((*cid_end != '"') && !isspace(*cid_end) && (cid_end < ptr)) - cid_end++; - - // copy tag and attributes up to src="cid: - StrBufAppendBufPlain(BodyArea, pBody, src - pBody, 0); - - // add in /webcit/mimepart//CID/ - // trailing / stops dumb URL filters getting excited - StrBufAppendPrintf(BodyArea, "/webcit/mimepart/%ld/", msgnum); - StrBufAppendBufPlain(BodyArea, cid_start, cid_end - cid_start, 0); - - if (ptr - cid_end > 0) - StrBufAppendBufPlain(BodyArea, cid_end + 1, ptr - cid_end, 0); - } - else { - StrBufAppendBufPlain(BodyArea, pBody, ptr - pBody, 0); - } - } - *ptr = '>'; - } - ++ptr; - if ((ptr == NULL) || (ptr >= msgend)) - break; - msgstart = ptr; - } - - // Any of these tags cause everything including and following - // the tag to be removed. - if ((!strncasecmp(ptr, "/HTML", 5)) || (!strncasecmp(ptr, "/BODY", 5))) { - --ptr; - msgend = ptr; - strcpy(ptr, ""); - } - - ++ptr; - } - if (msgstart > msg) { - strcpy(msg, msgstart); - } - - // Now go through the message, parsing tags as necessary. - converted_msg = NewStrBufPlain(NULL, content_length + 8192); - - // Convert foreign character sets to UTF-8 if necessary - if ((strcasecmp(charset, "us-ascii")) - && (strcasecmp(charset, "UTF-8")) - && (strcasecmp(charset, "")) - ) { - syslog(LOG_DEBUG, "Converting %s to UTF-8", charset); - ctdl_iconv_open("UTF-8", charset, &ic); - if (ic == (iconv_t) (-1)) { - syslog(LOG_WARNING, "%s:%d iconv_open() failed: %s", __FILE__, __LINE__, strerror(errno)); - } - } - if (Source == NULL) { - if (ic != (iconv_t) (-1)) { - ibuf = msg; - ibuflen = content_length; - obuflen = content_length + (content_length / 2); - obuf = (char *) malloc(obuflen); - osav = obuf; - iconv(ic, &ibuf, &ibuflen, &obuf, &obuflen); - content_length = content_length + (content_length / 2) - obuflen; - osav[content_length] = 0; - free(msg); - msg = osav; - iconv_close(ic); - } - } - else { - if (ic != (iconv_t) (-1)) { - StrBuf *Buf = NewStrBufPlain(NULL, StrLength(Source) + 8096);; - StrBufConvert(Source, Buf, &ic); - FreeStrBuf(&Buf); - iconv_close(ic); - msg = (char *) ChrPtr(Source); // TODO: get rid of this. - } - } - - // At this point, the message has been stripped down to - // only the content inside the tags, and has - // been converted to UTF-8 if it was originally in a foreign - // character set. The text is also guaranteed to be null - // terminated now. - - if (converted_msg == NULL) { - StrBufAppendPrintf(Target, "Error %d: %s
%s:%d", errno, strerror(errno), __FILE__, __LINE__); - goto BAIL; - } - - if (BodyArea != NULL) { // Any attributes that were declared in the tag - StrBufAppendBufPlain(converted_msg, HKEY("
tag - StrBufAppendBuf(converted_msg, BodyArea, 0); - StrBufAppendBufPlain(converted_msg, HKEY(">"), 0); - } - ptr = msg; - msgend = strchr(msg, 0); - while (ptr < msgend) { - - // Try to sanitize the html of any rogue scripts - if (!strncasecmp(ptr, "')))) { - // open external links to new window - StrBufAppendPrintf(converted_msg, new_window); - ptr = &ptr[8]; - } - else if ((treat_as_wiki) - && (strncasecmp(ptr, "'); - char *src; - // FIXME - handle this situation (maybe someone opened an ') - || (ptr[i] == '[') - || (ptr[i] == ']') - || (ptr[i] == '"') - || (ptr[i] == '\'') - ) - linklen = i; - // entity tag? - if (ptr[i] == '&') { - if ((ptr[i + 2] == ';') || - (ptr[i + 3] == ';') || - (ptr[i + 5] == ';') || (ptr[i + 6] == ';') || (ptr[i + 7] == ';')) - linklen = i; - } - if (linklen > 0) - break; - } - if (linklen > 0) { - char *ltreviewptr; - char *nbspreviewptr; - char linkedchar; - int len; - - len = linklen; - linkedchar = ptr[len]; - ptr[len] = '\0'; - // spot for some subject strings tinymce tends to give us. - ltreviewptr = strchr(ptr, '<'); - if (ltreviewptr != NULL) { - *ltreviewptr = '\0'; - linklen = ltreviewptr - ptr; - } - - nbspreviewptr = strstr(ptr, " "); - if (nbspreviewptr != NULL) { - // nbspreviewptr = '\0'; - linklen = nbspreviewptr - ptr; - } - if (ltreviewptr != 0) - *ltreviewptr = '<'; - - ptr[len] = linkedchar; - - content_length += (32 + linklen); - StrBufAppendPrintf(converted_msg, "%s\"", new_window); - StrBufAppendBufPlain(converted_msg, ptr, linklen, 0); - StrBufAppendPrintf(converted_msg, "\">"); - StrBufAppendBufPlain(converted_msg, ptr, linklen, 0); - ptr += linklen; - StrBufAppendPrintf(converted_msg, ""); - } - } - else { - StrBufAppendBufPlain(converted_msg, ptr, 1, 0); - ptr++; - } - - if ((ptr >= msg) && (ptr <= msgend)) { - // We need to know when we're inside a tag, - // so we don't turn things that look like URL's into - // links, when they're already links - or image sources. - if ((ptr > msg) && (*(ptr - 1) == '<')) { - ++brak; - } - if ((ptr > msg) && (*(ptr - 1) == '>')) { - --brak; - if ((scriptlevel == 0) && (script_start_pos >= 0)) { - StrBufCutRight(converted_msg, StrLength(converted_msg) - script_start_pos); - script_start_pos = (-1); - } - } - if (!strncasecmp(ptr, "", 3)) - --alevel; - } - } - - if (BodyArea != NULL) { - StrBufAppendBufPlain(converted_msg, HKEY("
"), 0); // Close the div where we declared attributes copied - FreeStrBuf(&BodyArea); // from the original tag - } - - // uncomment these two lines to override conversion - // memcpy(converted_msg, msg, content_length); - // output_length = content_length; - - // Output our big pile of markup - StrBufAppendBuf(Target, converted_msg, 0); - - BAIL: // A little trailing vertical whitespace... - StrBufAppendPrintf(Target, "
\n"); - - // Now give back the memory - FreeStrBuf(&converted_msg); - if ((msg != NULL) && (Source == NULL)) - free(msg); - return (Target); -} - - -// Look for URL's embedded in a buffer and make them linkable. We use a -// target window in order to keep the Citadel session in its own window. -void UrlizeText(StrBuf * Target, StrBuf * Source, StrBuf * WrkBuf) { - int len, UrlLen, Offset, TrailerLen; - const char *start, *end, *pos; - - FlushStrBuf(Target); - start = NULL; - len = StrLength(Source); - end = ChrPtr(Source) + len; - for (pos = ChrPtr(Source); (pos < end) && (start == NULL); ++pos) { - if (!strncasecmp(pos, "http://", 7)) - start = pos; - else if (!strncasecmp(pos, "ftp://", 6)) - start = pos; - } - - if (start == NULL) { - StrBufAppendBuf(Target, Source, 0); - return; - } - FlushStrBuf(WrkBuf); - - for (pos = ChrPtr(Source) + len; pos > start; --pos) { - if ((!isprint(*pos)) - || (isspace(*pos)) - || (*pos == '{') - || (*pos == '}') - || (*pos == '|') - || (*pos == '\\') - || (*pos == '^') - || (*pos == '[') - || (*pos == ']') - || (*pos == '`') - || (*pos == '<') - || (*pos == '>') - || (*pos == '(') - || (*pos == ')') - ) { - end = pos; - } - } - - UrlLen = end - start; - StrBufAppendBufPlain(WrkBuf, start, UrlLen, 0); - - Offset = start - ChrPtr(Source); - if (Offset != 0) - StrBufAppendBufPlain(Target, ChrPtr(Source), Offset, 0); - StrBufAppendPrintf(Target, "%ca href=%c%s%c TARGET=%c%s%c%c%s%c/A%c", - LB, QU, ChrPtr(WrkBuf), QU, QU, TARGET, QU, RB, ChrPtr(WrkBuf), LB, RB); - - TrailerLen = StrLength(Source) - (end - ChrPtr(Source)); - if (TrailerLen > 0) - StrBufAppendBufPlain(Target, end, TrailerLen, 0); -} - - -void url(char *buf, size_t bufsize) { - int len, UrlLen, Offset, TrailerLen, outpos; - char *start, *end, *pos; - char urlbuf[SIZ]; - char outbuf[SIZ]; - - start = NULL; - len = strlen(buf); - if (len > bufsize) { - syslog(LOG_WARNING, "URL: content longer than buffer!"); - return; - } - end = buf + len; - for (pos = buf; (pos < end) && (start == NULL); ++pos) { - if (!strncasecmp(pos, "http://", 7)) - start = pos; - if (!strncasecmp(pos, "ftp://", 6)) - start = pos; - } - - if (start == NULL) - return; - - for (pos = buf + len; pos > start; --pos) { - if ((!isprint(*pos)) - || (isspace(*pos)) - || (*pos == '{') - || (*pos == '}') - || (*pos == '|') - || (*pos == '\\') - || (*pos == '^') - || (*pos == '[') - || (*pos == ']') - || (*pos == '`') - || (*pos == '<') - || (*pos == '>') - || (*pos == '(') - || (*pos == ')') - ) { - end = pos; - } - } - - UrlLen = end - start; - if (UrlLen > sizeof(urlbuf)) { - syslog(LOG_WARNING, "URL: content longer than buffer!"); - return; - } - memcpy(urlbuf, start, UrlLen); - urlbuf[UrlLen] = '\0'; - - Offset = start - buf; - if ((Offset != 0) && (Offset < sizeof(outbuf))) - memcpy(outbuf, buf, Offset); - outpos = snprintf(&outbuf[Offset], sizeof(outbuf) - Offset, - "%ca href=%c%s%c TARGET=%c%s%c%c%s%c/A%c", LB, QU, urlbuf, QU, QU, TARGET, QU, RB, urlbuf, LB, RB); - if (outpos >= sizeof(outbuf) - Offset) { - syslog(LOG_WARNING, "URL: content longer than buffer!"); - return; - } - - TrailerLen = len - (end - start); - if (TrailerLen > 0) - memcpy(outbuf + Offset + outpos, end, TrailerLen); - if (Offset + outpos + TrailerLen > bufsize) { - syslog(LOG_WARNING, "URL: content longer than buffer!"); - return; - } - memcpy(buf, outbuf, Offset + outpos + TrailerLen); - *(buf + Offset + outpos + TrailerLen) = '\0'; -} diff --git a/webcit-ng/http.c b/webcit-ng/http.c deleted file mode 100644 index 55c335eff..000000000 --- a/webcit-ng/http.c +++ /dev/null @@ -1,327 +0,0 @@ -// This module handles HTTP transactions. -// -// Copyright (c) 1996-2022 by the citadel.org team -// -// This program is open source software. Use, duplication, or -// disclosure are subject to the GNU General Public License v3. - -#include "webcit.h" - -// Write data to the HTTP client. Encrypt if necessary. -int client_write(struct client_handle *ch, char *buf, int nbytes) { - if (is_https) { - return client_write_ssl(ch, buf, nbytes); - } - else { - return write(ch->sock, buf, nbytes); - } -} - - -// Read data from the HTTP client. Decrypt if necessary. -// Returns number of bytes read, or -1 to indicate an error. -int client_read(struct client_handle *ch, char *buf, int nbytes) { - if (is_https) { - return client_read_ssl(ch, buf, nbytes); - } - else { - int bytes_received = 0; - int bytes_this_block = 0; - while (bytes_received < nbytes) { - bytes_this_block = read(ch->sock, &buf[bytes_received], nbytes - bytes_received); - if (bytes_this_block < 1) { - return (-1); - } - bytes_received += bytes_this_block; - } - return (nbytes); - } -} - - -// Read a newline-terminated line of text from the client. -// Implemented in terms of client_read() and is therefore transparent... -// Returns the string length or -1 for error. -int client_readline(struct client_handle *ch, char *buf, int maxbytes) { - int len = 0; - int c = 0; - - if (buf == NULL) { - return (-1); - } - - while (len < maxbytes) { - c = client_read(ch, &buf[len], 1); - if (c <= 0) { - syslog(LOG_DEBUG, "Socket error or zero-length read"); - return (-1); - } - if (buf[len] == '\n') { - if ((len > 0) && (buf[len - 1] == '\r')) { - --len; - } - buf[len] = 0; - return (len); - } - ++len; - } - return (len); -} - - -// printf() type function to send data to the web client. -void client_printf(struct client_handle *ch, const char *format, ...) { - va_list arg_ptr; - StrBuf *Buf = NewStrBuf(); - - va_start(arg_ptr, format); - StrBufVAppendPrintf(Buf, format, arg_ptr); - va_end(arg_ptr); - - client_write(ch, (char *) ChrPtr(Buf), StrLength(Buf)); - FreeStrBuf(&Buf); -} - - -// Push one new header into the response of an HTTP request. -// When completed, the HTTP transaction now owns the memory allocated for key and val. -void add_response_header(struct http_transaction *h, char *key, char *val) { - struct keyval new_response_header; - new_response_header.key = key; - new_response_header.val = val; - array_append(h->response_headers, &new_response_header); -} - - -// Entry point for this layer. Socket is connected. If running as an HTTPS -// server, SSL has already been negotiated. Now perform one transaction. -void perform_one_http_transaction(struct client_handle *ch) { - char buf[1024]; - int len; - int lines_read = 0; - struct http_transaction h; - char *c, *d; - struct sockaddr_in sin; - int i; // general purpose iterator variable - - memset(&h, 0, sizeof h); - h.request_headers = array_new(sizeof(struct keyval)); - h.request_parms = array_new(sizeof(struct keyval)); - h.response_headers = array_new(sizeof(struct keyval)); - - while (len = client_readline(ch, buf, sizeof buf), (len > 0)) { - ++lines_read; - - if (lines_read == 1) { // First line is method and URL. - c = strchr(buf, ' '); // IGnore the HTTP-version. - if (c == NULL) { - h.method = strdup("NULL"); - h.url = strdup("/"); - } - else { - *c = 0; - h.method = strdup(buf); - ++c; - d = c; - c = strchr(d, ' '); - if (c != NULL) { - *c = 0; - } - ++c; - h.url = strdup(d); - } - } - else { // Subsequent lines are headers. - c = strchr(buf, ':'); // Header line folding is obsolete so we don't support it. - if (c != NULL) { - - struct keyval new_request_header; - *c = 0; - new_request_header.key = strdup(buf); - ++c; - new_request_header.val = strdup(c); - striplt(new_request_header.key); - striplt(new_request_header.val); - array_append(h.request_headers, &new_request_header); -#ifdef DEBUG_HTTP - syslog(LOG_DEBUG, "\033[1m\033[35m{ %s: %s\033[0m", new_request_header.key, new_request_header.val); -#endif - } - } - - } - - // If the URL had any query parameters in it, parse them out now. - char *p = (h.url ? strchr(h.url, '?') : NULL); - if (p) { - *p++ = 0; // insert a null to remove parameters from the URL - char *tok, *saveptr = NULL; - for (tok = strtok_r(p, "&", &saveptr); tok!=NULL; tok = strtok_r(NULL, "&", &saveptr)) { - char *eq = strchr(tok, '='); - if (eq) { - *eq++ = 0; - unescape_input(eq); - struct keyval kv; - kv.key = strdup(tok); - kv.val = strdup(eq); - array_append(h.request_parms, &kv); -#ifdef DEBUG_HTTP - syslog(LOG_DEBUG, "\033[1m\033[33m| %s = %s\033[0m", kv.key, kv.val); -#endif - } - } - } - - // build up the site prefix, such as https://foo.bar.com:4343 - h.site_prefix = malloc(256); - strcpy(h.site_prefix, (is_https ? "https://" : "http://")); - char *hostheader = header_val(&h, "Host"); - if (hostheader) { - strcat(h.site_prefix, hostheader); - } - else { - strcat(h.site_prefix, "127.0.0.1"); - } - socklen_t llen = sizeof(sin); - if (getsockname(ch->sock, (struct sockaddr *) &sin, &llen) >= 0) { - sprintf(&h.site_prefix[strlen(h.site_prefix)], ":%d", ntohs(sin.sin_port)); - } - - // Now try to read in the request body (if one is present) - if (len < 0) { - syslog(LOG_DEBUG, "Client disconnected"); - } - else { -//#ifdef DEBUG_HTTP - syslog(LOG_DEBUG, "\033[33m\033[1m< %s %s\033[0m", h.method, h.url); -//#endif - - // If there is a request body, read it now. - char *ccl = header_val(&h, "Content-Length"); - if (ccl) { - h.request_body_length = atol(ccl); - } - if (h.request_body_length > 0) { - syslog(LOG_DEBUG, "Reading request body (%ld bytes)", h.request_body_length); - h.request_body = malloc(h.request_body_length); - client_read(ch, h.request_body, h.request_body_length); - - // Write the entire request body to stderr -- not what you want during normal operation. - #ifdef BODY_TO_STDERR - write(2, HKEY("\033[31m")); - write(2, h.request_body, h.request_body_length); - write(2, HKEY("\033[0m\n")); - #endif - - } - - // Now pass control up to the next layer to perform the request. - perform_request(&h); - - // Write the entire response body to stderr -- not what you want during normal operation. - #ifdef BODY_TO_STDERR - write(2, HKEY("\033[32m")); - write(2, h.response_body, h.response_body_length); - write(2, HKEY("\033[0m\n")); - #endif - - // Output the results back to the client. -#ifdef DEBUG_HTTP - syslog(LOG_DEBUG, "\033[33m\033[1m> %03d %s\033[0m", h.response_code, h.response_string); -#endif - client_printf(ch, "HTTP/1.1 %03d %s\r\n", h.response_code, h.response_string); - client_printf(ch, "Connection: close\r\n"); - client_printf(ch, "Content-Length: %ld\r\n", h.response_body_length); - char *datestring = http_datestring(time(NULL)); - if (datestring) { - client_printf(ch, "Date: %s\r\n", datestring); - free(datestring); - } - - client_printf(ch, "Content-encoding: identity\r\n"); // change if we gzip/deflate - int number_of_response_headers = array_len(h.response_headers); - for (i=0; ikey, kv->val); -#endif - client_printf(ch, "%s: %s\r\n", kv->key, kv->val); - } - client_printf(ch, "\r\n"); - if ((h.response_body_length > 0) && (h.response_body != NULL)) { - client_write(ch, h.response_body, h.response_body_length); - } - } - - // free the transaction memory - while (array_len(h.request_headers) > 0) { - struct keyval *kv = array_get_element_at(h.request_headers, 0); - if (kv->key) free(kv->key); - if (kv->val) free(kv->val); - array_delete_element_at(h.request_headers, 0); - } - array_free(h.request_headers); - while (array_len(h.request_parms) > 0) { - struct keyval *kv = array_get_element_at(h.request_parms, 0); - if (kv->key) free(kv->key); - if (kv->val) free(kv->val); - array_delete_element_at(h.request_parms, 0); - } - array_free(h.request_parms); - while (array_len(h.response_headers) > 0) { - struct keyval *kv = array_get_element_at(h.response_headers, 0); - if (kv->key) free(kv->key); - if (kv->val) free(kv->val); - array_delete_element_at(h.response_headers, 0); - } - array_free(h.response_headers); - if (h.method) { - free(h.method); - } - if (h.url) { - free(h.url); - } - if (h.request_body) { - free(h.request_body); - } - if (h.response_string) { - free(h.response_string); - } - if (h.site_prefix) { - free(h.site_prefix); - } -} - - -// Utility function to fetch a specific header from a completely read-in request. -// Returns the value of the requested header, or NULL if the specified header was not sent. -// The caller does NOT own the memory of the returned pointer, but can count on the pointer -// to still be valid through the end of the transaction. -char *header_val(struct http_transaction *h, char *requested_header) { - struct keyval *kv; - int i; - for (i=0; irequest_headers); ++i) { - kv = array_get_element_at(h->request_headers, i); - if (!strcasecmp(kv->key, requested_header)) { - return (kv->val); - } - } - return (NULL); -} - - -// Utility function to fetch a specific URL parameter from a completely read-in request. -// Returns the value of the requested parameter, or NULL if the specified parameter was not sent. -// The caller does NOT own the memory of the returned pointer, but can count on the pointer -// to still be valid through the end of the transaction. -char *get_url_param(struct http_transaction *h, char *requested_param) { - struct keyval *kv; - int i; - for (i=0; irequest_parms); ++i) { - kv = array_get_element_at(h->request_parms, i); - if (!strcasecmp(kv->key, requested_param)) { - return (kv->val); - } - } - return (NULL); -} diff --git a/webcit-ng/main.c b/webcit-ng/main.c deleted file mode 100644 index 031369131..000000000 --- a/webcit-ng/main.c +++ /dev/null @@ -1,129 +0,0 @@ -// Main entry point for the program. -// -// Copyright (c) 1996-2022 by the citadel.org team -// -// This program is open source software. Use, duplication, or -// disclosure are subject to the GNU General Public License v3. - -#include "webcit.h" // All other headers are included from this header. - -char *ctdl_dir = CTDL_DIR; - -// Main entry point for the web server. -int main(int argc, char **argv) { - int webserver_port = WEBSERVER_PORT; - char *webserver_interface = WEBSERVER_INTERFACE; - int running_as_daemon = 0; - int webserver_protocol = WEBSERVER_HTTP; - int a; - char *pid_file = NULL; - - // Parse command line - while ((a = getopt(argc, argv, "u:h:i:p:t:T:B:x:g:dD:G:cfsS:Z:v:")) != EOF) - switch (a) { - case 'u': - setuid(atoi(optarg)); - break; - case 'h': - ctdl_dir = strdup(optarg); - break; - case 'd': - running_as_daemon = 1; - break; - case 'D': - running_as_daemon = 1; - pid_file = strdup(optarg); - break; - case 'g': - // FIXME set up the default landing page - break; - case 'B': - case 't': - case 'x': - case 'T': - case 'v': - // The above options are no longer used, but ignored so old scripts don't break - break; - case 'i': - webserver_interface = optarg; - break; - case 'p': - webserver_port = atoi(optarg); - break; - case 'Z': - // FIXME when gzip is added, disable it if this flag is set - break; - case 'f': - //follow_xff = 1; - break; - case 'c': - break; - case 's': - is_https = 1; - webserver_protocol = WEBSERVER_HTTPS; - break; - case 'S': - is_https = 1; - webserver_protocol = WEBSERVER_HTTPS; - //ssl_cipher_list = strdup(optarg); - break; - case 'G': - //DumpTemplateI18NStrings = 1; - //I18nDump = NewStrBufPlain(HKEY("int templatestrings(void)\n{\n")); - //I18nDumpFile = optarg; - break; - default: - fprintf(stderr, "usage:\nwebcit " - "[-i ip_addr] [-p http_port] " - "[-c] [-f] " - "[-d] [-Z] [-G i18ndumpfile] " - "[-u uid] [-h homedirectory] " - "[-D daemonizepid] [-v] " - "[-g defaultlandingpage] [-B basesize] " - "[-s] [-S cipher_suites]" - "[-h citadel_server_directory]\n" - ); - return 1; - } - - while (optind < argc) { - ctdl_dir = strdup(argv[optind++]); - } - - // Start the logger - openlog("webcit", (running_as_daemon ? (LOG_PID) : (LOG_PID | LOG_PERROR)), LOG_DAEMON); - - // Tell 'em who's in da house - syslog(LOG_NOTICE, "MAKE WEBCIT GREAT AGAIN!"); - syslog(LOG_NOTICE, "Copyright (C) 1996-2022 by the citadel.org team"); - syslog(LOG_NOTICE, " "); - syslog(LOG_NOTICE, "This program is open source software: you can redistribute it and/or"); - syslog(LOG_NOTICE, "modify it under the terms of the GNU General Public License, version 3."); - syslog(LOG_NOTICE, " "); - syslog(LOG_NOTICE, "This program is distributed in the hope that it will be useful,"); - syslog(LOG_NOTICE, "but WITHOUT ANY WARRANTY; without even the implied warranty of"); - syslog(LOG_NOTICE, "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the"); - syslog(LOG_NOTICE, "GNU General Public License for more details."); - syslog(LOG_NOTICE, " "); - - // Ensure that we are linked to the correct version of libcitadel - if (libcitadel_version_number() < LIBCITADEL_VERSION_NUMBER) { - syslog(LOG_INFO, " You are running libcitadel version %d", libcitadel_version_number()); - syslog(LOG_INFO, "WebCit was compiled against version %d", LIBCITADEL_VERSION_NUMBER); - return (1); - } - - // Go into the background if we were asked to run as a daemon - if (running_as_daemon) { - daemon(1, 0); - if (pid_file != NULL) { - FILE *pfp = fopen(pid_file, "w"); - if (pfp) { - fprintf(pfp, "%d\n", getpid()); - fclose(pfp); - } - } - } - - return webserver(webserver_interface, webserver_port, webserver_protocol); -} diff --git a/webcit-ng/messages.c b/webcit-ng/messages.c deleted file mode 100644 index 1ef039f82..000000000 --- a/webcit-ng/messages.c +++ /dev/null @@ -1,264 +0,0 @@ -// -// Message base functions -// -// Copyright (c) 1996-2022 by the citadel.org team -// -// This program is open source software. Use, duplication, or -// disclosure are subject to the GNU General Public License v3. - -#include "webcit.h" - - -// Given an encoded UID, translate that to an unencoded Citadel EUID and -// then search for it in the current room. Return a message number or -1 -// if not found. -long locate_message_by_uid(struct ctdlsession *c, char *uid) { - char buf[1024]; - - ctdl_printf(c, "EUID %s", uid); - ctdl_readline(c, buf, sizeof buf); - if (buf[0] == '2') { - return (atol(&buf[4])); - - } - - // Ugly hack to handle Mozilla Thunderbird, try stripping ".ics" if present - if (!strcasecmp(&uid[strlen(uid) - 4], ".ics")) { - safestrncpy(buf, uid, sizeof buf); - buf[strlen(buf) - 4] = 0; - ctdl_printf(c, "EUID %s", buf); - ctdl_readline(c, buf, sizeof buf); - if (buf[0] == '2') { - return (atol(&buf[4])); - - } - } - - return (-1); -} - - -// DAV delete an object in a room. -void dav_delete_message(struct http_transaction *h, struct ctdlsession *c, long msgnum) { - ctdl_delete_msgs(c, &msgnum, 1); - h->response_code = 204; - h->response_string = strdup("no content"); -} - - -// GET method directly on a message in a room -void dav_get_message(struct http_transaction *h, struct ctdlsession *c, long msgnum) { - char buf[1024]; - int in_body = 0; - int encoding = 0; - StrBuf *Body = NULL; - - ctdl_printf(c, "MSG2 %ld", msgnum); - ctdl_readline(c, buf, sizeof buf); - if (buf[0] != '1') { - do_404(h); - return; - } - - char *etag = malloc(20); - if (etag != NULL) { - sprintf(etag, "%ld", msgnum); - add_response_header(h, strdup("ETag"), etag); // http_transaction now owns this memory - } - - while (ctdl_readline(c, buf, sizeof buf), strcmp(buf, "000")) { - if (IsEmptyStr(buf) && (in_body == 0)) { - in_body = 1; - Body = NewStrBuf(); - } - else if (in_body == 0) { - char *k = buf; - char *v = strchr(buf, ':'); - if (v) { - *v = 0; - ++v; - striplt(v); // we now have a key (k) and a value (v) - if ((!strcasecmp(k, "content-type")) // fields which can be passed from RFC822 to HTTP as-is - || (!strcasecmp(k, "date")) - ) { - add_response_header(h, strdup(k), strdup(v)); - } - else if (!strcasecmp(k, "content-transfer-encoding")) { - if (!strcasecmp(v, "base64")) { - encoding = 'b'; - } - else if (!strcasecmp(v, "quoted-printable")) { - encoding = 'q'; - } - } - } - } - else if ((in_body == 1) && (Body != NULL)) { - StrBufAppendPrintf(Body, "%s\n", buf); - } - } - - h->response_code = 200; - h->response_string = strdup("OK"); - - if (Body != NULL) { - if (encoding == 'q') { - h->response_body = malloc(StrLength(Body)); - if (h->response_body != NULL) { - h->response_body_length = - CtdlDecodeQuotedPrintable(h->response_body, (char *) ChrPtr(Body), StrLength(Body)); - } - FreeStrBuf(&Body); - } - else if (encoding == 'b') { - h->response_body = malloc(StrLength(Body)); - if (h->response_body != NULL) { - h->response_body_length = CtdlDecodeBase64(h->response_body, ChrPtr(Body), StrLength(Body)); - } - FreeStrBuf(&Body); - } - else { - h->response_body_length = StrLength(Body); - h->response_body = SmashStrBuf(&Body); - } - } -} - - -// PUT a message into a room -void dav_put_message(struct http_transaction *h, struct ctdlsession *c, char *euid, long old_msgnum) { - char buf[1024]; - char *content_type = NULL; - int n; - long new_msgnum; - char new_euid[1024]; - char response_string[1024]; - - if ((h->request_body == NULL) || (h->request_body_length < 1)) { - do_404(h); // Refuse to post a null message - return; - } - - char *wefw = get_url_param(h, "wefw"); // references - if (!wefw) wefw = ""; - char *subj = get_url_param(h, "subj"); // subject - if (!subj) subj = ""; - - // Mode 4 will give us metadata back after upload - ctdl_printf(c, "ENT0 1|||4|%s||1|||||%s|", subj, wefw); - ctdl_readline(c, buf, sizeof buf); - if (buf[0] != '8') { - h->response_code = 502; - h->response_string = strdup("bad gateway"); - add_response_header(h, strdup("Content-type"), strdup("text/plain")); - h->response_body = strdup(buf); - h->response_body_length = strlen(h->response_body); - return; - } - - // Remember, ctdl_printf() appends \n on its own, so when adding a CRLF newline, only use \r - // Or for a blank line, use ctdl_write() with \r\n - - content_type = header_val(h, "Content-type"); - ctdl_printf(c, "Content-type: %s\r", (content_type ? content_type : "application/octet-stream")); - ctdl_write(c, HKEY("\r\n")); - ctdl_write(c, h->request_body, h->request_body_length); - if (h->request_body[h->request_body_length] != '\n') { - ctdl_write(c, HKEY("\r\n")); - } - ctdl_printf(c, "000"); - - // Now handle the response from the Citadel server. - - n = 0; - new_msgnum = 0; - strcpy(new_euid, ""); - strcpy(response_string, ""); - - while (ctdl_readline(c, buf, sizeof buf), strcmp(buf, "000")) - switch (n++) { - case 0: - new_msgnum = atol(buf); - break; - case 1: - safestrncpy(response_string, buf, sizeof response_string); - syslog(LOG_DEBUG, "new_msgnum=%ld (%s)\n", new_msgnum, buf); - break; - case 2: - safestrncpy(new_euid, buf, sizeof new_euid); - break; - default: - break; - } - - // Tell the client what happened. - - // Citadel failed in some way? - char *new_location = malloc(1024); - if ((new_msgnum < 0L) || (new_location == NULL)) { - h->response_code = 502; - h->response_string = strdup("bad gateway"); - add_response_header(h, strdup("Content-type"), strdup("text/plain")); - h->response_body = strdup(response_string); - h->response_body_length = strlen(h->response_body); - return; - } - - char *etag = malloc(20); - if (etag != NULL) { - sprintf(etag, "%ld", new_msgnum); - add_response_header(h, strdup("ETag"), etag); // http_transaction now owns this memory - } - - char esc_room[1024]; - char esc_euid[1024]; - urlesc(esc_room, sizeof esc_room, c->room); - urlesc(esc_euid, sizeof esc_euid, new_euid); - snprintf(new_location, 1024, "/ctdl/r/%s/%s", esc_room, esc_euid); - add_response_header(h, strdup("Location"), new_location); // http_transaction now owns this memory - - if (old_msgnum <= 0) { - h->response_code = 201; // We created this item for the first time. - h->response_string = strdup("created"); - } - else { - h->response_code = 204; // We modified an existing item. - h->response_string = strdup("no content"); - - // The item we replaced has probably already been deleted by - // the Citadel server, but we'll do this anyway, just in case. - ctdl_delete_msgs(c, &old_msgnum, 1); - } - -} - - -// Download a single component of a MIME-encoded message -void download_mime_component(struct http_transaction *h, struct ctdlsession *c, long msgnum, char *partnum) { - char buf[1024]; - char content_type[1024]; - - ctdl_printf(c, "DLAT %ld|%s", msgnum, partnum); - ctdl_readline(c, buf, sizeof buf); - if (buf[0] != '6') { - do_404(h); // too bad, so sad, go away - } - // Server response is going to be: 6XX length|-1|filename|content-type|charset - h->response_body_length = extract_int(&buf[4], 0); - extract_token(content_type, buf, 3, '|', sizeof content_type); - - h->response_body = malloc(h->response_body_length + 1); - int bytes = 0; - int thisblock; - do { - thisblock = read(c->sock, &h->response_body[bytes], (h->response_body_length - bytes)); - bytes += thisblock; - syslog(LOG_DEBUG, "Bytes read: %d of %d", (int) bytes, (int) h->response_body_length); - } while ((bytes < h->response_body_length) && (thisblock >= 0)); - h->response_body[h->response_body_length] = 0; // null terminate it just for good measure - syslog(LOG_DEBUG, "content type: %s", content_type); - - add_response_header(h, strdup("Content-type"), strdup(content_type)); - h->response_code = 200; - h->response_string = strdup("OK"); -} diff --git a/webcit-ng/request.c b/webcit-ng/request.c deleted file mode 100644 index 605520e68..000000000 --- a/webcit-ng/request.c +++ /dev/null @@ -1,183 +0,0 @@ -// This module sits directly above the HTTP layer. By the time we get here, -// an HTTP request has been fully received and parsed. Control is passed up -// to this layer to actually perform the request. We then fill in the response -// and pass control back down to the HTTP layer to output the response back to -// the client. -// -// Copyright (c) 1996-2022 by the citadel.org team -// -// This program is open source software. Use, duplication, or -// disclosure are subject to the GNU General Public License v3. - -#include "webcit.h" - - -// Not found! Wowzers. -void do_404(struct http_transaction *h) { - h->response_code = 404; - h->response_string = strdup("NOT FOUND"); - add_response_header(h, strdup("Content-type"), strdup("text/plain")); - h->response_body = strdup("404 NOT FOUND\r\n"); - h->response_body_length = strlen(h->response_body); -} - - -// Precondition failed (such as if-match) -void do_412(struct http_transaction *h) { - h->response_code = 412; - h->response_string = strdup("PRECONDITION FAILED"); -} - - -// Succeed with no output -void do_204(struct http_transaction *h) { - h->response_code = 204; - h->response_string = strdup("No content"); -} - - -// We throw an HTTP error "502 bad gateway" when we need to connect to Citadel, but can't. -void do_502(struct http_transaction *h) { - h->response_code = 502; - h->response_string = strdup("bad gateway"); - add_response_header(h, strdup("Content-type"), strdup("text/plain")); - h->response_body = strdup(_("This program was unable to connect or stay connected to the Citadel server. Please report this problem to your system administrator.")); - h->response_body_length = strlen(h->response_body); -} - - -// Tell the client to authenticate using HTTP-AUTH (RFC 2617) -void request_http_authenticate(struct http_transaction *h) { - h->response_code = 401; - h->response_string = strdup("Unauthorized"); - add_response_header(h, strdup("WWW-Authenticate"), strdup("Basic realm=\"Citadel Server\"")); -} - - -// Easy and fun utility function to throw a redirect. -void http_redirect(struct http_transaction *h, char *to_where) { - syslog(LOG_DEBUG, "Redirecting to: %s", to_where); - h->response_code = 302; - h->response_string = strdup("Moved Temporarily"); - add_response_header(h, strdup("Location"), strdup(to_where)); - add_response_header(h, strdup("Content-type"), strdup("text/plain")); - h->response_body = strdup(to_where); - h->response_body_length = strlen(h->response_body); -} - - -// perform_request() is the entry point for *every* HTTP transaction. -void perform_request(struct http_transaction *h) { - struct ctdlsession *c; - - // Determine which code path to take based on the beginning of the URL. - // This is implemented as a series of strncasecmp() calls rather than a - // lookup table in order to make the code more readable. - - if (IsEmptyStr(h->url)) { // Sanity check - do_404(h); - return; - } - - // Right about here is where we should try to handle anything that doesn't start - // with the /ctdl/ prefix. - // Root (/) ... - - if ((!strcmp(h->url, "/")) && (!strcasecmp(h->method, "GET"))) { - http_redirect(h, "/ctdl/s/index.html"); - return; - } - - // Legacy URL patterns (/readnew?gotoroom=xxx&start_reading_at=yyy) ... - // Direct room name (/my%20blog) ... - - // HTTP-01 challenge [RFC5785 section 3, RFC8555 section 9.2] - if (!strncasecmp(h->url, HKEY("/.well-known"))) { // Static content - output_static(h); - return; - } - - if (!strcasecmp(h->url, "/favicon.ico")) { - output_static(h); - return; - } - - // Everything below this line is strictly REST URL patterns. - - if (strncasecmp(h->url, HKEY("/ctdl/"))) { // Reject non-REST - do_404(h); - return; - } - - if (!strncasecmp(h->url, HKEY("/ctdl/s/"))) { // Static content - output_static(h); - return; - } - - if (h->url[7] != '/') { - do_404(h); - return; - } - - // Anything below this line: - // 1. Is in the format of /ctdl/?/* - // 2. Requires a connection to a Citadel server. - - c = connect_to_citadel(h); - if (c == NULL) { - do_502(h); - return; - } - - // WebDAV methods like OPTIONS and PROPFIND *require* a logged-in session, - // even if the Citadel server allows anonymous access. - if (IsEmptyStr(c->auth)) { - if ( (!strcasecmp(h->method, "OPTIONS")) - || (!strcasecmp(h->method, "PROPFIND")) - || (!strcasecmp(h->method, "REPORT")) - || (!strcasecmp(h->method, "DELETE")) - ) { - request_http_authenticate(h); - disconnect_from_citadel(c); - return; - } - } - - // Break down the URL by path and send the request to the appropriate part of the program. - switch (h->url[6]) { - case 'a': // /ctdl/a/ == RESTful path to admin functions - ctdl_a(h, c); - break; - case 'c': // /ctdl/c/ == misc Citadel server commands - ctdl_c(h, c); - break; - case 'f': // /ctdl/f/ == RESTful path to floors - ctdl_f(h, c); - break; - case 'r': // /ctdl/r/ == RESTful path to rooms - ctdl_r(h, c); - break; - case 'u': // /ctdl/u/ == RESTful path to users - ctdl_u(h, c); - break; - default: - do_404(h); - } - - // Are we in an authenticated session? If so, set a cookie so we stay logged in. - if (!IsEmptyStr(c->auth)) { - char koekje[AUTH_MAX]; - char *exp = http_datestring(time(NULL) + (86400 * 30)); - snprintf(koekje, AUTH_MAX, "wcauth=%s; path=/ctdl/; Expires=%s", c->auth, exp); // warn - free(exp); - add_response_header(h, strdup("Set-Cookie"), strdup(koekje)); - } - - // Durlng development we are foiling the browser cache completely. In production we'll be more selective. - add_response_header(h, strdup("Cache-Control"), strdup("no-store, must-revalidate")); - add_response_header(h, strdup("Pragma"), strdup("no-cache")); - add_response_header(h, strdup("Expires"), strdup("0")); - - // Unbind from our Citadel server connection. - disconnect_from_citadel(c); -} diff --git a/webcit-ng/room_functions.c b/webcit-ng/room_functions.c deleted file mode 100644 index 67b3e9082..000000000 --- a/webcit-ng/room_functions.c +++ /dev/null @@ -1,679 +0,0 @@ -// Room functions -// -// Copyright (c) 1996-2022 by the citadel.org team -// -// This program is open source software. Use, duplication, or -// disclosure are subject to the GNU General Public License v3. - -#include "webcit.h" - - -// Return a "zero-terminated" array of message numbers in the current room. -// Caller owns the memory and must free it. Returns NULL if any problems. -long *get_msglist(struct ctdlsession *c, char *which_msgs) { - char buf[1024]; - long *msglist = NULL; - int num_msgs = 0; - int num_alloc = 0; - - ctdl_printf(c, "MSGS %s", which_msgs); - ctdl_readline(c, buf, sizeof(buf)); - if (buf[0] == '1') { - do { - if (num_msgs >= num_alloc) { - if (num_alloc == 0) { - num_alloc = 1024; - msglist = malloc(num_alloc * sizeof(long)); - } - else { - num_alloc *= 2; - msglist = realloc(msglist, num_alloc * sizeof(long)); - } - } - ctdl_readline(c, buf, sizeof(buf)); - msglist[num_msgs++] = atol(buf); - } while (strcmp(buf, "000")); // this makes the last element a "0" terminator - } - return msglist; -} - - -// Supplied with a list of potential matches from an If-Match: or If-None-Match: header, and -// a message number (which we always use as the entity tag in Citadel), return nonzero if the -// message number matches any of the supplied tags in the string. -int match_etags(char *taglist, long msgnum) { - int num_tags = num_tokens(taglist, ','); - int i = 0; - char tag[1024]; - - if (msgnum <= 0) { // no msgnum? no match. - return (0); - } - - for (i = 0; i < num_tags; ++i) { - extract_token(tag, taglist, i, ',', sizeof tag); - striplt(tag); - char *lq = (strchr(tag, '"')); - char *rq = (strrchr(tag, '"')); - if (lq < rq) { // has two double quotes - strcpy(rq, ""); - strcpy(tag, ++lq); - } - striplt(tag); - if (!strcmp(tag, "*")) { // wildcard match - return (1); - } - long tagmsgnum = atol(tag); - if ((tagmsgnum > 0) && (tagmsgnum == msgnum)) { // match - return (1); - } - } - - return (0); // no match -} - - -// Client is requesting a STAT (name and modification time) of the current room -void json_stat(struct http_transaction *h, struct ctdlsession *c) { - char buf[1024]; - char field[1024]; - - ctdl_printf(c, "STAT"); - ctdl_readline(c, buf, sizeof(buf)); - if (buf[0] == '2') { - JsonValue *j = NewJsonObject(HKEY("stat")); - extract_token(field, &buf[4], 0, '|', sizeof field); - JsonObjectAppend(j, NewJsonPlainString(HKEY("name"), field, -1)); - JsonObjectAppend(j, NewJsonNumber(HKEY("room_mtime"), extract_long(&buf[4], 1))); - - StrBuf *sj = NewStrBuf(); - SerializeJson(sj, j, 1); // '1' == free the source array - add_response_header(h, strdup("Content-type"), strdup("application/json")); - h->response_code = 200; - h->response_string = strdup("OK"); - h->response_body_length = StrLength(sj); - h->response_body = SmashStrBuf(&sj); - } - else { - do_404(h); - } - return; -} - - -// Client is requesting a mailbox summary of the current room -void json_mailbox(struct http_transaction *h, struct ctdlsession *c) { - char buf[1024]; - char field[1024]; - JsonValue *j = NewJsonArray(HKEY("msgs")); - - ctdl_printf(c, "MSGS ALL|||9"); // "9" as the fourth parameter delivers a mailbox summary - ctdl_readline(c, buf, sizeof(buf)); - if (buf[0] == '1') { - while (ctdl_readline(c, buf, sizeof(buf)), (strcmp(buf, "000"))) { - utf8ify_rfc822_string(buf); - JsonValue *jmsg = NewJsonObject(HKEY("message")); - JsonObjectAppend(jmsg, NewJsonNumber(HKEY("msgnum"), extract_long(buf, 0))); - JsonObjectAppend(jmsg, NewJsonNumber(HKEY("time"), extract_long(buf, 1))); - extract_token(field, buf, 2, '|', sizeof field); - JsonObjectAppend(jmsg, NewJsonPlainString(HKEY("author"), field, -1)); - extract_token(field, buf, 4, '|', sizeof field); - JsonObjectAppend(jmsg, NewJsonPlainString(HKEY("addr"), field, -1)); - extract_token(field, buf, 5, '|', sizeof field); - JsonObjectAppend(jmsg, NewJsonPlainString(HKEY("subject"), field, -1)); - JsonObjectAppend(jmsg, NewJsonNumber(HKEY("msgidhash"), extract_long(buf, 6))); - extract_token(field, buf, 7, '|', sizeof field); - JsonObjectAppend(jmsg, NewJsonPlainString(HKEY("references"), field, -1)); - JsonArrayAppend(j, jmsg); // add the message to the array - } - } - - StrBuf *sj = NewStrBuf(); - SerializeJson(sj, j, 1); // '1' == free the source array - - add_response_header(h, strdup("Content-type"), strdup("application/json")); - h->response_code = 200; - h->response_string = strdup("OK"); - h->response_body_length = StrLength(sj); - h->response_body = SmashStrBuf(&sj); - return; -} - - -// Client is requesting a message list -void json_msglist(struct http_transaction *h, struct ctdlsession *c, char *which) { - int i = 0; - long *msglist = get_msglist(c, which); - JsonValue *j = NewJsonArray(HKEY("msgs")); - - if (msglist != NULL) { - for (i = 0; msglist[i] > 0; ++i) { - JsonArrayAppend(j, NewJsonNumber(HKEY("m"), msglist[i])); - } - free(msglist); - } - - StrBuf *sj = NewStrBuf(); - SerializeJson(sj, j, 1); // '1' == free the source array - - add_response_header(h, strdup("Content-type"), strdup("application/json")); - h->response_code = 200; - h->response_string = strdup("OK"); - h->response_body_length = StrLength(sj); - h->response_body = SmashStrBuf(&sj); - return; -} - - -// Client is requesting the room info banner -void read_room_info_banner(struct http_transaction *h, struct ctdlsession *c) { - char buf[1024]; - - ctdl_printf(c, "RINF"); - ctdl_readline(c, buf, sizeof(buf)); - if (buf[0] == '1') { - StrBuf *info = NewStrBuf(); - while (ctdl_readline(c, buf, sizeof(buf)), (strcmp(buf, "000"))){ - StrBufAppendPrintf(info, "%s\n", buf); - } - add_response_header(h, strdup("Content-type"), strdup("text/plain")); - h->response_code = 200; - h->response_string = strdup("OK"); - h->response_body_length = StrLength(info); - h->response_body = SmashStrBuf(&info); - return; - } - else { - do_404(h); - } -} - - -// Client is setting the "Last Read Pointer" (marking all messages as "seen" up to this message) -void set_last_read_pointer(struct http_transaction *h, struct ctdlsession *c) { - char cbuf[1024]; - ctdl_printf(c, "SLRP %d", atoi(get_url_param(h, "last"))); - ctdl_readline(c, cbuf, sizeof(cbuf)); - if (cbuf[0] == '2') { - do_204(h); - } - else { - do_404(h); - } -} - - -// Client requested an object in a room. -void object_in_room(struct http_transaction *h, struct ctdlsession *c) { - char buf[1024]; - long msgnum = (-1); - char unescaped_euid[1024]; - - extract_token(buf, h->url, 4, '/', sizeof buf); - - if (!strcasecmp(buf, "mailbox")) { // Client is requesting a mailbox summary - json_mailbox(h, c); - return; - } - - if (!strcasecmp(buf, "stat")) { // Client is requesting a stat command (name and modification time) - json_stat(h, c); - return; - } - - if (!strncasecmp(buf, "msgs.", 5)) { // Client is requesting a list of message numbers - unescape_input(&buf[5]); - json_msglist(h, c, &buf[5]); - return; - } - - if (!strcasecmp(buf, "info.txt")) { // Client is requesting the room info banner - read_room_info_banner(h, c); - return; - } - - if (!strcasecmp(buf, "slrp")) { // Set the Last Read Pointer - set_last_read_pointer(h, c); - return; - } - - // If we get to this point, the client is requesting a specific object *in* the room. - - if ((c->room_default_view == VIEW_CALENDAR) // room types where objects are referenced by EUID - || (c->room_default_view == VIEW_TASKS) - || (c->room_default_view == VIEW_ADDRESSBOOK) - ) { - safestrncpy(unescaped_euid, buf, sizeof unescaped_euid); - unescape_input(unescaped_euid); - msgnum = locate_message_by_uid(c, unescaped_euid); - } - else { // otherwise the object is being referenced by message number - msgnum = atol(buf); - } - - // All methods except PUT require the message to already exist - if ((msgnum <= 0) && (strcasecmp(h->method, "PUT"))) { - do_404(h); - } - - // If we get to this point we have a valid message number in an accessible room. - syslog(LOG_DEBUG, "msgnum is %ld, method is %s", msgnum, h->method); - - // A sixth component in the URL can be one of two things: - // (1) a MIME part specifier, in which case the client wants to download that component within the message - // (2) a content-type, in which ase the client wants us to try to render it a certain way - if (num_tokens(h->url, '/') == 6) { - extract_token(buf, h->url, 5, '/', sizeof buf); - if (!IsEmptyStr(buf)) { - if (!strcasecmp(buf, "json")) { - json_render_one_message(h, c, msgnum); - } - else { - download_mime_component(h, c, msgnum, buf); - } - return; - } - } - - // Ok, we want a full message, but first let's check for the if[-none]-match headers. - char *if_match = header_val(h, "If-Match"); - if ((if_match != NULL) && (!match_etags(if_match, msgnum))) { - do_412(h); - return; - } - - char *if_none_match = header_val(h, "If-None-Match"); - if ((if_none_match != NULL) && (match_etags(if_none_match, msgnum))) { - do_412(h); - return; - } - - // DOOOOOO ITTTTT!!! - - if (!strcasecmp(h->method, "DELETE")) { - dav_delete_message(h, c, msgnum); - } - else if (!strcasecmp(h->method, "GET")) { - dav_get_message(h, c, msgnum); - } - else if (!strcasecmp(h->method, "PUT")) { - dav_put_message(h, c, unescaped_euid, msgnum); - } - else { - do_404(h); // Got this far but the method made no sense? Bummer. - } - -} - - -// Called by the_room_itself() when the HTTP method is REPORT -void report_the_room_itself(struct http_transaction *h, struct ctdlsession *c) { - if (c->room_default_view == VIEW_CALENDAR) { - caldav_report(h, c); // CalDAV REPORTs ... fmgwac - return; - } - - do_404(h); // future implementations like CardDAV will require code paths here -} - - -// Called by the_room_itself() when the HTTP method is OPTIONS -void options_the_room_itself(struct http_transaction *h, struct ctdlsession *c) { - h->response_code = 200; - h->response_string = strdup("OK"); - if (c->room_default_view == VIEW_CALENDAR) { - add_response_header(h, strdup("DAV"), strdup("1, calendar-access")); // offer CalDAV - } - else if (c->room_default_view == VIEW_ADDRESSBOOK) { - add_response_header(h, strdup("DAV"), strdup("1, addressbook")); // offer CardDAV - } - else { - add_response_header(h, strdup("DAV"), strdup("1")); // ordinary WebDAV for all other room types - } - add_response_header(h, strdup("Allow"), strdup("OPTIONS, PROPFIND, GET, PUT, REPORT, DELETE")); -} - - -// Called by the_room_itself() when the HTTP method is PROPFIND -void propfind_the_room_itself(struct http_transaction *h, struct ctdlsession *c) { - char *e; - long timestamp; - int dav_depth = (header_val(h, "Depth") ? atoi(header_val(h, "Depth")) : INT_MAX); - syslog(LOG_DEBUG, "Client PROPFIND requested depth: %d", dav_depth); - StrBuf *Buf = NewStrBuf(); - - StrBufAppendPrintf(Buf, "" - ""); - - // Transmit the collection resource - StrBufAppendPrintf(Buf, ""); - StrBufAppendPrintf(Buf, ""); - StrBufXMLEscAppend(Buf, NULL, h->site_prefix, strlen(h->site_prefix), 0); - StrBufAppendPrintf(Buf, "/ctdl/r/"); - StrBufXMLEscAppend(Buf, NULL, c->room, strlen(c->room), 0); - StrBufAppendPrintf(Buf, ""); - - StrBufAppendPrintf(Buf, ""); - StrBufAppendPrintf(Buf, "HTTP/1.1 200 OK"); - StrBufAppendPrintf(Buf, ""); - StrBufAppendPrintf(Buf, ""); - StrBufXMLEscAppend(Buf, NULL, c->room, strlen(c->room), 0); - StrBufAppendPrintf(Buf, ""); - - StrBufAppendPrintf(Buf, ""); // empty owner ought to be legal; see rfc3744 section 5.1 - - StrBufAppendPrintf(Buf, ""); - switch (c->room_default_view) { - case VIEW_CALENDAR: - StrBufAppendPrintf(Buf, ""); // RFC4791 section 4.2 - break; - } - StrBufAppendPrintf(Buf, ""); - - int enumerate_by_euid = 0; // nonzero if messages will be retrieved by euid instead of msgnum - switch (c->room_default_view) { - case VIEW_CALENDAR: // RFC4791 section 5.2 - StrBufAppendPrintf(Buf, ""); - StrBufAppendPrintf(Buf, ""); - StrBufAppendPrintf(Buf, ""); - StrBufAppendPrintf(Buf, ""); - enumerate_by_euid = 1; - break; - case VIEW_TASKS: // RFC4791 section 5.2 - StrBufAppendPrintf(Buf, ""); - StrBufAppendPrintf(Buf, ""); - StrBufAppendPrintf(Buf, ""); - StrBufAppendPrintf(Buf, ""); - enumerate_by_euid = 1; - break; - case VIEW_ADDRESSBOOK: // FIXME put some sort of CardDAV crapola here when we implement it - enumerate_by_euid = 1; - break; - case VIEW_WIKI: // FIXME invent "WikiDAV" ? The versioning stuff in DAV could be useful. - enumerate_by_euid = 1; - break; - } - - // FIXME get the mtime - // StrBufAppendPrintf(Buf, ""); - // escputs(datestring); - // StrBufAppendPrintf(Buf, ""); - - StrBufAppendPrintf(Buf, ""); - StrBufAppendPrintf(Buf, ""); - StrBufAppendPrintf(Buf, "\n"); - - // If a depth greater than zero was specified, transmit the collection listing - // BEGIN COLLECTION - if (dav_depth > 0) { - long *msglist = get_msglist(c, "ALL"); - if (msglist) { - int i; - for (i = 0; (msglist[i] > 0); ++i) { - if ((i % 10) == 0) - syslog(LOG_DEBUG, "PROPFIND enumerated %d messages", i); - e = NULL; // EUID gets stored here - timestamp = 0; - - char cbuf[1024]; - ctdl_printf(c, "MSG0 %ld|3", msglist[i]); - ctdl_readline(c, cbuf, sizeof(cbuf)); - if (cbuf[0] == '1') - while (ctdl_readline(c, cbuf, sizeof(cbuf)), strcmp(cbuf, "000")) { - if ((enumerate_by_euid) && (!strncasecmp(cbuf, "exti=", 5))) { - // e = strdup(&cbuf[5]); - int elen = (2 * strlen(&cbuf[5])); - e = malloc(elen); - urlesc(e, elen, &cbuf[5]); - } - if (!strncasecmp(cbuf, "time=", 5)) { - timestamp = atol(&cbuf[5]); - } - } - if (e == NULL) { - e = malloc(20); - sprintf(e, "%ld", msglist[i]); - } - StrBufAppendPrintf(Buf, ""); - - // Generate the 'href' tag for this message - StrBufAppendPrintf(Buf, ""); - StrBufXMLEscAppend(Buf, NULL, h->site_prefix, strlen(h->site_prefix), 0); - StrBufAppendPrintf(Buf, "/ctdl/r/"); - StrBufXMLEscAppend(Buf, NULL, c->room, strlen(c->room), 0); - StrBufAppendPrintf(Buf, "/"); - StrBufXMLEscAppend(Buf, NULL, e, strlen(e), 0); - StrBufAppendPrintf(Buf, ""); - StrBufAppendPrintf(Buf, ""); - StrBufAppendPrintf(Buf, "HTTP/1.1 200 OK"); - StrBufAppendPrintf(Buf, ""); - - switch (c->room_default_view) { - case VIEW_CALENDAR: - StrBufAppendPrintf(Buf, - "text/calendar; component=vevent"); - break; - case VIEW_TASKS: - StrBufAppendPrintf(Buf, - "text/calendar; component=vtodo"); - break; - case VIEW_ADDRESSBOOK: - StrBufAppendPrintf(Buf, "text/x-vcard"); - break; - } - - if (timestamp > 0) { - char *datestring = http_datestring(timestamp); - if (datestring) { - StrBufAppendPrintf(Buf, ""); - StrBufXMLEscAppend(Buf, NULL, datestring, strlen(datestring), 0); - StrBufAppendPrintf(Buf, ""); - free(datestring); - } - if (enumerate_by_euid) { // FIXME ajc 2017oct30 should this be inside the timestamp conditional? - StrBufAppendPrintf(Buf, "\"%ld\"", msglist[i]); - } - } - StrBufAppendPrintf(Buf, "\n"); - free(e); - } - free(msglist); - }; - } - // END COLLECTION - - StrBufAppendPrintf(Buf, "\n"); - - add_response_header(h, strdup("Content-type"), strdup("text/xml")); - h->response_code = 207; - h->response_string = strdup("Multi-Status"); - h->response_body_length = StrLength(Buf); - h->response_body = SmashStrBuf(&Buf); -} - -// some good examples here -// http://blogs.nologin.es/rickyepoderi/index.php?/archives/14-Introducing-CalDAV-Part-I.html - - -// Called by the_room_itself() when the HTTP method is PROPFIND -void get_the_room_itself(struct http_transaction *h, struct ctdlsession *c) { - JsonValue *j = NewJsonObject(HKEY("gotoroom")); - - JsonObjectAppend(j, NewJsonPlainString(HKEY("name"), c->room, -1)); - JsonObjectAppend(j, NewJsonNumber(HKEY("current_view"), c->room_current_view)); - JsonObjectAppend(j, NewJsonNumber(HKEY("default_view"), c->room_default_view)); - JsonObjectAppend(j, NewJsonNumber(HKEY("is_room_aide"), c->is_room_aide)); - JsonObjectAppend(j, NewJsonNumber(HKEY("can_delete_messages"), c->can_delete_messages)); - JsonObjectAppend(j, NewJsonNumber(HKEY("new_messages"), c->new_messages)); - JsonObjectAppend(j, NewJsonNumber(HKEY("total_messages"), c->total_messages)); - JsonObjectAppend(j, NewJsonNumber(HKEY("last_seen"), c->last_seen)); - JsonObjectAppend(j, NewJsonNumber(HKEY("room_mtime"), c->room_mtime)); - JsonObjectAppend(j, NewJsonNumber(HKEY("new_mail"), c->new_mail)); - - StrBuf *sj = NewStrBuf(); - SerializeJson(sj, j, 1); // '1' == free the source array - - add_response_header(h, strdup("Content-type"), strdup("application/json")); - h->response_code = 200; - h->response_string = strdup("OK"); - h->response_body_length = StrLength(sj); - h->response_body = SmashStrBuf(&sj); - return; -} - - -// Handle REST/DAV requests for the room itself (such as /ctdl/r/roomname -// or /ctdl/r/roomname/ but *not* specific objects within the room) -void the_room_itself(struct http_transaction *h, struct ctdlsession *c) { - - // OPTIONS method on the room itself usually is a DAV client assessing what's here. - if (!strcasecmp(h->method, "OPTIONS")) { - options_the_room_itself(h, c); - return; - } - - // PROPFIND method on the room itself could be looking for a directory - if (!strcasecmp(h->method, "PROPFIND")) { - propfind_the_room_itself(h, c); - return; - } - - // REPORT method on the room itself is probably the dreaded CalDAV tower-of-crapola - if (!strcasecmp(h->method, "REPORT")) { - report_the_room_itself(h, c); - return; - } - - // GET method on the room itself is an API call, possibly from our JavaScript front end - if (!strcasecmp(h->method, "get")) { - get_the_room_itself(h, c); - return; - } - - // we probably want a "go to this room" for interactive access - do_404(h); -} - - -// Dispatcher for "/ctdl/r" and "/ctdl/r/" for the room list -void room_list(struct http_transaction *h, struct ctdlsession *c) { - char buf[1024]; - char roomname[1024]; - - ctdl_printf(c, "LKRA"); - ctdl_readline(c, buf, sizeof(buf)); - if (buf[0] != '1') { - do_502(h); - return; - } - - JsonValue *j = NewJsonArray(HKEY("lkra")); - while (ctdl_readline(c, buf, sizeof(buf)), strcmp(buf, "000")) { - - // 0 |1 |2 |3 |4 |5 |6 |7 |8 - // name|QRflags|QRfloor|QRorder|QRflags2|ra|current_view|default_view|mtime - JsonValue *jr = NewJsonObject(HKEY("room")); - - extract_token(roomname, buf, 0, '|', sizeof roomname); - JsonObjectAppend(jr, NewJsonPlainString(HKEY("name"), roomname, -1)); - - JsonObjectAppend(jr, NewJsonNumber(HKEY("floor"), extract_int(buf, 2))); - JsonObjectAppend(jr, NewJsonNumber(HKEY("rorder"), extract_int(buf, 3))); - - int ra = extract_int(buf, 5); - JsonObjectAppend(jr, NewJsonBool(HKEY("known"), (ra & UA_KNOWN))); - JsonObjectAppend(jr, NewJsonBool(HKEY("hasnewmsgs"), (ra & UA_HASNEWMSGS))); - - JsonObjectAppend(jr, NewJsonNumber(HKEY("current_view"), extract_int(buf, 6))); - JsonObjectAppend(jr, NewJsonNumber(HKEY("default_view"), extract_int(buf, 7))); - JsonObjectAppend(jr, NewJsonNumber(HKEY("mtime"), extract_int(buf, 8))); - - JsonArrayAppend(j, jr); // add the room to the array - } - - StrBuf *sj = NewStrBuf(); - SerializeJson(sj, j, 1); // '1' == free the source array - - add_response_header(h, strdup("Content-type"), strdup("application/json")); - h->response_code = 200; - h->response_string = strdup("OK"); - h->response_body_length = StrLength(sj); - h->response_body = SmashStrBuf(&sj); -} - - -// Dispatcher for paths starting with /ctdl/r/ -void ctdl_r(struct http_transaction *h, struct ctdlsession *c) { - char requested_roomname[128]; - char buf[1024]; - long room_flags = 0; - long room_flags2 = 0; - - // All room-related functions require being "in" the room specified. Are we in that room already? - extract_token(requested_roomname, h->url, 3, '/', sizeof requested_roomname); - unescape_input(requested_roomname); - - if (IsEmptyStr(requested_roomname)) { // /ctdl/r/ - room_list(h, c); - return; - } - // If not, try to go there. - if (strcasecmp(requested_roomname, c->room)) { - ctdl_printf(c, "GOTO %s", requested_roomname); - ctdl_readline(c, buf, sizeof(buf)); - if (buf[0] == '2') { - // buf[3] will indicate whether any instant messages are waiting - extract_token(c->room, &buf[4], 0, '|', sizeof c->room); - c->new_messages = extract_int(&buf[4], 1); - c->total_messages = extract_int(&buf[4], 2); - // 3 (int)info Info flag: set to nonzero if the user needs to read this room's info file - room_flags = extract_long(&buf[4], 3); // Various flags associated with this room. - // 5 (long)CC->room.QRhighest The highest message number present in this room - c->last_seen = extract_long(&buf[4], 6); // The highest message number the user has read in this room - // 7 (int)rmailflag Boolean flag: 1 if this is a Mail> room, 0 otherwise. - c->is_room_aide = extract_int(&buf[4], 8); - c->new_mail = extract_int(&buf[4], 9); // the number of new messages in the user's INBOX - // 10 (int)CC->room.QRfloor The floor number this room resides on - c->room_current_view = extract_int(&buf[4], 11); - c->room_default_view = extract_int(&buf[4], 12); - // 13 (int)is_trash Boolean flag: 1 if this is the user's Trash folder, 0 otherwise. - room_flags2 = extract_long(&buf[4], 14); // More flags associated with this room. - c->room_mtime = extract_long(&buf[4], 15); // Timestamp of the last write activity in this room - - // If any of these three conditions are met, let the client know it has permission to delete messages. - if ((c->is_room_aide) || (room_flags & QR_MAILBOX) || (room_flags2 & QR2_COLLABDEL)) { - c->can_delete_messages = 1; - } - else { - c->can_delete_messages = 0; - } - } - else { - do_404(h); - return; - } - } - // At this point our Citadel client session is "in" the specified room. - - if (num_tokens(h->url, '/') == 4) { // /ctdl/r/roomname - the_room_itself(h, c); - return; - } - - extract_token(buf, h->url, 4, '/', sizeof buf); - if (num_tokens(h->url, '/') == 5) { - if (IsEmptyStr(buf)) { - the_room_itself(h, c); // /ctdl/r/roomname/ ( same as /ctdl/r/roomname ) - } - else { - object_in_room(h, c); // /ctdl/r/roomname/object - } - return; - } - if (num_tokens(h->url, '/') == 6) { - object_in_room(h, c); // /ctdl/r/roomname/object/ or possibly /ctdl/r/roomname/object/component - return; - } - // If we get to this point, the client specified a valid room but requested an action we don't know how to perform. - do_404(h); -} diff --git a/webcit-ng/server/admin_functions.c b/webcit-ng/server/admin_functions.c new file mode 100644 index 000000000..2c2d85f1f --- /dev/null +++ b/webcit-ng/server/admin_functions.c @@ -0,0 +1,105 @@ +// Admin functions +// +// Copyright (c) 1996-2022 by the citadel.org team +// +// This program is open source software. Use, duplication, or +// disclosure are subject to the GNU General Public License v3. + +#include "webcit.h" + + +// /ctdl/a/login is called when a user is trying to log in +void try_login(struct http_transaction *h, struct ctdlsession *c) { + char buf[1024]; + char auth[AUTH_MAX]; + char username[256]; + char password[256]; + int login_success = 0; + + extract_token(username, h->request_body, 0, '|', sizeof username); + extract_token(password, h->request_body, 1, '|', sizeof password); + + snprintf(buf, sizeof buf, "%s:%s", username, password); + CtdlEncodeBase64(auth, buf, strlen(buf), BASE64_NO_LINEBREAKS); + + syslog(LOG_DEBUG, "try_login(username='%s',password=(%d bytes))", username, (int) strlen(password)); + + ctdl_printf(c, "LOUT"); // log out, in case we were logged in + ctdl_readline(c, buf, sizeof(buf)); // ignore the result + memset(c->auth, 0, AUTH_MAX); // if this connection had auth, it doesn't now. + memset(c->whoami, 0, 64); // if this connection had auth, it doesn't now. + login_success = login_to_citadel(c, auth, buf); // Now try logging in to Citadel + + JsonValue *j = NewJsonObject(HKEY("login")); // Compose a JSON object with the results + if (buf[0] == '2') { + JsonObjectAppend(j, NewJsonBool(HKEY("result"), 1)); + JsonObjectAppend(j, NewJsonPlainString(HKEY("message"), "logged in", -1)); + extract_token(username, &buf[4], 0, '|', sizeof username); // This will have the proper capitalization etc. + JsonObjectAppend(j, NewJsonPlainString(HKEY("fullname"), username, -1)); + JsonObjectAppend(j, NewJsonNumber(HKEY("axlevel"), extract_int(&buf[4], 1) )); + JsonObjectAppend(j, NewJsonNumber(HKEY("timescalled"), extract_long(&buf[4], 2) )); + JsonObjectAppend(j, NewJsonNumber(HKEY("posted"), extract_long(&buf[4], 3) )); + JsonObjectAppend(j, NewJsonNumber(HKEY("usernum"), extract_long(&buf[4], 5) )); + JsonObjectAppend(j, NewJsonNumber(HKEY("previous_login"), extract_long(&buf[4], 6) )); + } + else { + JsonObjectAppend(j, NewJsonBool(HKEY("result"), 0)); + JsonObjectAppend(j, NewJsonPlainString(HKEY("message"), &buf[4], -1)); + } + StrBuf *sj = NewStrBuf(); + SerializeJson(sj, j, 1); // '1' == free the source object + + add_response_header(h, strdup("Content-type"), strdup("application/json")); + h->response_code = 200; + h->response_string = strdup("OK"); + h->response_body_length = StrLength(sj); + h->response_body = SmashStrBuf(&sj); +} + + +// /ctdl/a/logout is called when a user is trying to log out. Don't use this as an ajax. +void logout(struct http_transaction *h, struct ctdlsession *c) { + char buf[1024]; + char auth[AUTH_MAX]; + char username[256]; + char password[256]; + int login_success = 0; + + ctdl_printf(c, "LOUT"); // log out + ctdl_readline(c, buf, sizeof(buf)); // ignore the result + strcpy(c->auth, "x"); + memset(c->whoami, 0, 64); // if this connection had auth, it doesn't now. + + http_redirect(h, "/ctdl/s/index.html"); // go back where we started :) +} + + +// /ctdl/a/whoami returns the name of the currently logged in user, or an empty string if not logged in +void whoami(struct http_transaction *h, struct ctdlsession *c) { + h->response_code = 200; + h->response_string = strdup("OK"); + add_response_header(h, strdup("Content-type"), strdup("text/plain")); + h->response_body = strdup(c->whoami); + h->response_body_length = strlen(h->response_body); +} + + +// Dispatcher for paths starting with /ctdl/a/ +void ctdl_a(struct http_transaction *h, struct ctdlsession *c) { + if (!strcasecmp(h->url, "/ctdl/a/login")) { // log in + try_login(h, c); + return; + } + + if (!strcasecmp(h->url, "/ctdl/a/logout")) { // log out + logout(h, c); + return; + } + + if (!strcasecmp(h->url, "/ctdl/a/whoami")) { // return display name of user + whoami(h, c); + return; + } + + do_404(h); // unknown +} diff --git a/webcit-ng/server/caldav_reports.c b/webcit-ng/server/caldav_reports.c new file mode 100644 index 000000000..9bcca613f --- /dev/null +++ b/webcit-ng/server/caldav_reports.c @@ -0,0 +1,270 @@ +// +// This file contains functions which handle all of the CalDAV "REPORT" queries +// specified in RFC4791 section 7. +// +// Copyright (c) 2022 by the citadel.org team +// +// This program is open source software. Use, duplication, or +// disclosure are subject to the GNU General Public License v3. + +#include "webcit.h" + + +// A CalDAV REPORT can only be one type. This is stored in the report_type member. +enum cr_type { + cr_calendar_query, + cr_calendar_multiget, + cr_freebusy_query +}; + + +// Data type for CalDAV Report Parameters. +// As we slog our way through the XML we learn what the client is asking for +// and build up the contents of this data type. +struct cr_parms { + int tag_nesting_level; // not needed, just kept for pretty-printing + enum cr_type report_type; // which RFC4791 section 7 REPORT are we generating + StrBuf *Chardata; // XML chardata in between tags is built up here + StrBuf *Hrefs; // list of items requested by a calendar-multiget report +}; + + +// XML parser callback +void caldav_xml_start(void *data, const char *el, const char **attr) { + struct cr_parms *crp = (struct cr_parms *) data; + int i; + + // syslog(LOG_DEBUG, "CALDAV ELEMENT START: <%s> %d", el, crp->tag_nesting_level); + + for (i = 0; attr[i] != NULL; i += 2) { + syslog(LOG_DEBUG, " Attribute '%s' = '%s'", attr[i], attr[i + 1]); + } + + if (!strcasecmp(el, "urn:ietf:params:xml:ns:caldav:calendar-multiget")) { + crp->report_type = cr_calendar_multiget; + } + + else if (!strcasecmp(el, "urn:ietf:params:xml:ns:caldav:calendar-query")) { + crp->report_type = cr_calendar_query; + } + + else if (!strcasecmp(el, "urn:ietf:params:xml:ns:caldav:free-busy-query")) { + crp->report_type = cr_freebusy_query; + } + + ++crp->tag_nesting_level; +} + + +// XML parser callback +void caldav_xml_end(void *data, const char *el) { + struct cr_parms *crp = (struct cr_parms *) data; + --crp->tag_nesting_level; + + if (crp->Chardata != NULL) { + // syslog(LOG_DEBUG, "CALDAV CHARDATA : %s", ChrPtr(crp->Chardata)); + } + // syslog(LOG_DEBUG, "CALDAV ELEMENT END : <%s> %d", el, crp->tag_nesting_level); + + if ((!strcasecmp(el, "DAV::href")) || (!strcasecmp(el, "DAV:href"))) { + if (crp->Hrefs == NULL) { // append crp->Chardata to crp->Hrefs + crp->Hrefs = NewStrBuf(); + } + else { + StrBufAppendBufPlain(crp->Hrefs, HKEY("|"), 0); + } + StrBufAppendBuf(crp->Hrefs, crp->Chardata, 0); + } + + if (crp->Chardata != NULL) { // Tag is closed; chardata is now out of scope. + FreeStrBuf(&crp->Chardata); // Free the buffer. + crp->Chardata = NULL; + } +} + + +// XML parser callback +void caldav_xml_chardata(void *data, const XML_Char * s, int len) { + struct cr_parms *crp = (struct cr_parms *) data; + + if (crp->Chardata == NULL) { + crp->Chardata = NewStrBuf(); + } + + StrBufAppendBufPlain(crp->Chardata, s, len, 0); + + return; +} + + +// Called by caldav_response() to fetch a message (by number) in the current room, +// and return only the icalendar data as a StrBuf. Returns NULL if not found. +// +// NOTE: this function expects that "MSGP text/calendar" was issued at the beginning +// of a REPORT operation to set our preferred MIME type to calendar data. +StrBuf *fetch_ical(struct ctdlsession * c, long msgnum) { + char buf[1024]; + StrBuf *Buf = NULL; + + ctdl_printf(c, "MSG4 %ld", msgnum); + ctdl_readline(c, buf, sizeof(buf)); + if (buf[0] != '1') { + return NULL; + } + + while (ctdl_readline(c, buf, sizeof(buf)), strcmp(buf, "000")) { + if (Buf != NULL) { // already in body + StrBufAppendPrintf(Buf, "%s\n", buf); + } + else if (IsEmptyStr(buf)) { // beginning of body + Buf = NewStrBuf(); + } + } + + return Buf; + +// webcit[13039]: msgn=53CE87AF-00392161@uncensored.citadel.org +// webcit[13039]: path=IGnatius T Foobar +// webcit[13039]: time=1208008800 +// webcit[13039]: from=IGnatius T Foobar +// webcit[13039]: room=0000000001.Calendar +// webcit[13039]: node=uncnsrd +// webcit[13039]: hnod=Uncensored +// webcit[13039]: exti=040000008200E00074C5B7101A82E0080000000080C728F83E84C801000000000000000010000000E857E0DC57F53947ADF0BB91EE3A502F +// webcit[13039]: subj==?UTF-8?B?V2VzbGV5J3MgYmlydGhkYXkgcGFydHk= +// webcit[13039]: ?= +// webcit[13039]: part=||1||text/calendar|1127|| +// webcit[13039]: text +// webcit[13039]: Content-type: text/calendar +// webcit[13039]: Content-length: 1127 +// webcit[13039]: Content-transfer-encoding: 7bit +// webcit[13039]: X-Citadel-MSG4-Partnum: 1 +// webcit[13039]: +// webcit[13039]: BEGIN:VCALENDAR +} + + +// Called by caldav_report() to output a single item. +// Our policy is to throw away the list of properties the client asked for, and just send everything. +void caldav_response(struct http_transaction *h, struct ctdlsession *c, StrBuf * ReportOut, StrBuf * ThisHref) { + long msgnum; + StrBuf *Caldata = NULL; + char *euid; + + euid = strrchr(ChrPtr(ThisHref), '/'); + if (euid != NULL) { + ++euid; + } + else { + euid = (char *) ChrPtr(ThisHref); + } + + char *unescaped_euid = strdup(euid); + if (!unescaped_euid) { + return; + } + unescape_input(unescaped_euid); + + StrBufAppendPrintf(ReportOut, ""); + StrBufAppendPrintf(ReportOut, ""); + StrBufXMLEscAppend(ReportOut, ThisHref, NULL, 0, 0); + StrBufAppendPrintf(ReportOut, ""); + StrBufAppendPrintf(ReportOut, ""); + + msgnum = locate_message_by_uid(c, unescaped_euid); + free(unescaped_euid); + if (msgnum > 0) { + Caldata = fetch_ical(c, msgnum); + } + + if (Caldata != NULL) { + // syslog(LOG_DEBUG, "caldav_response(%s) 200 OK", ChrPtr(ThisHref)); + StrBufAppendPrintf(ReportOut, ""); + StrBufAppendPrintf(ReportOut, "HTTP/1.1 200 OK"); + StrBufAppendPrintf(ReportOut, ""); + StrBufAppendPrintf(ReportOut, ""); + StrBufAppendPrintf(ReportOut, ""); + StrBufAppendPrintf(ReportOut, "%ld", msgnum); + StrBufAppendPrintf(ReportOut, ""); + StrBufAppendPrintf(ReportOut, ""); + StrBufXMLEscAppend(ReportOut, Caldata, NULL, 0, 0); + StrBufAppendPrintf(ReportOut, ""); + StrBufAppendPrintf(ReportOut, ""); + FreeStrBuf(&Caldata); + Caldata = NULL; + } + else { + // syslog(LOG_DEBUG, "caldav_response(%s) 404 not found", ChrPtr(ThisHref)); + StrBufAppendPrintf(ReportOut, ""); + StrBufAppendPrintf(ReportOut, "HTTP/1.1 404 not found"); + StrBufAppendPrintf(ReportOut, ""); + } + + StrBufAppendPrintf(ReportOut, ""); + StrBufAppendPrintf(ReportOut, ""); +} + + +// Called by report_the_room_itself() in room_functions.c when a CalDAV REPORT method +// is requested on a calendar room. We fire up an XML Parser to decode the request and +// hopefully produce the correct output. +void caldav_report(struct http_transaction *h, struct ctdlsession *c) { + struct cr_parms crp; + char buf[1024]; + + memset(&crp, 0, sizeof(struct cr_parms)); + + XML_Parser xp = XML_ParserCreateNS("UTF-8", ':'); + if (xp == NULL) { + syslog(LOG_INFO, "Cannot create XML parser!"); + do_404(h); + return; + } + + XML_SetElementHandler(xp, caldav_xml_start, caldav_xml_end); + XML_SetCharacterDataHandler(xp, caldav_xml_chardata); + XML_SetUserData(xp, &crp); + XML_SetDefaultHandler(xp, NULL); // Disable internal entity expansion to prevent "billion laughs attack" + XML_Parse(xp, h->request_body, h->request_body_length, 1); + XML_ParserFree(xp); + + if (crp.Chardata != NULL) { // Discard any trailing chardata ... normally nothing here + FreeStrBuf(&crp.Chardata); + crp.Chardata = NULL; + } + + // We're going to make a lot of MSG4 calls, and the preferred MIME type we want is "text/calendar". + // The iCalendar standard is mature now, and we are no longer interested in text/x-vcal or application/ics. + ctdl_printf(c, "MSGP text/calendar"); + ctdl_readline(c, buf, sizeof buf); + + // Now begin the REPORT. + syslog(LOG_DEBUG, "CalDAV REPORT type is: %d", crp.report_type); + StrBuf *ReportOut = NewStrBuf(); + StrBufAppendPrintf(ReportOut, + "" + "" + ); + + if (crp.Hrefs != NULL) { // Output all qualifying calendar items! + StrBuf *ThisHref = NewStrBuf(); + const char *pvset = NULL; + while (StrBufExtract_NextToken(ThisHref, crp.Hrefs, &pvset, '|') >= 0) { + caldav_response(h, c, ReportOut, ThisHref); + } + FreeStrBuf(&ThisHref); + FreeStrBuf(&crp.Hrefs); + crp.Hrefs = NULL; + } + + StrBufAppendPrintf(ReportOut, "\n"); // End the REPORT. + + add_response_header(h, strdup("Content-type"), strdup("text/xml")); + h->response_code = 207; + h->response_string = strdup("Multi-Status"); + h->response_body_length = StrLength(ReportOut); + h->response_body = SmashStrBuf(&ReportOut); +} diff --git a/webcit-ng/server/ctdl_commands.c b/webcit-ng/server/ctdl_commands.c new file mode 100644 index 000000000..6420a8750 --- /dev/null +++ b/webcit-ng/server/ctdl_commands.c @@ -0,0 +1,94 @@ +// +// Copyright (c) 1996-2022 by the citadel.org team +// +// This program is open source software. Use, duplication, or +// disclosure are subject to the GNU General Public License v3. + +#include "webcit.h" + + +// /ctdl/c/info returns a JSON representation of the output of an INFO command. +void serv_info(struct http_transaction *h, struct ctdlsession *c) { + char buf[1024]; + + ctdl_printf(c, "INFO"); + ctdl_readline(c, buf, sizeof(buf)); + if (buf[0] != '1') { + do_502(h); + return; + } + + JsonValue *j = NewJsonObject(HKEY("serv_info")); + int i = 0; + while (ctdl_readline(c, buf, sizeof(buf)), strcmp(buf, "000")) + switch (i++) { + case 0: + JsonObjectAppend(j, NewJsonNumber(HKEY("serv_pid"), atol(buf))); + break; + case 1: + JsonObjectAppend(j, NewJsonPlainString(HKEY("serv_nodename"), buf, -1)); + break; + case 2: + JsonObjectAppend(j, NewJsonPlainString(HKEY("serv_humannode"), buf, -1)); + break; + case 3: + JsonObjectAppend(j, NewJsonPlainString(HKEY("serv_fqdn"), buf, -1)); + break; + case 4: + JsonObjectAppend(j, NewJsonPlainString(HKEY("serv_software"), buf, -1)); + break; + case 5: + JsonObjectAppend(j, NewJsonNumber(HKEY("serv_rev_level"), atol(buf))); + break; + case 6: + JsonObjectAppend(j, NewJsonPlainString(HKEY("serv_bbs_city"), buf, -1)); + break; + case 7: + JsonObjectAppend(j, NewJsonPlainString(HKEY("serv_sysadm"), buf, -1)); + break; + case 14: + JsonObjectAppend(j, NewJsonBool(HKEY("serv_supports_ldap"), atoi(buf))); + break; + case 15: + JsonObjectAppend(j, NewJsonBool(HKEY("serv_newuser_disabled"), atoi(buf))); + break; + case 16: + JsonObjectAppend(j, NewJsonPlainString(HKEY("serv_default_cal_zone"), buf, -1)); + break; + case 20: + JsonObjectAppend(j, NewJsonBool(HKEY("serv_supports_sieve"), atoi(buf))); + break; + case 21: + JsonObjectAppend(j, NewJsonBool(HKEY("serv_fulltext_enabled"), atoi(buf))); + break; + case 22: + JsonObjectAppend(j, NewJsonPlainString(HKEY("serv_svn_revision"), buf, -1)); + break; + case 23: + JsonObjectAppend(j, NewJsonBool(HKEY("serv_supports_openid"), atoi(buf))); + break; + case 24: + JsonObjectAppend(j, NewJsonBool(HKEY("serv_supports_guest"), atoi(buf))); + break; + } + + StrBuf *sj = NewStrBuf(); + SerializeJson(sj, j, 1); // '1' == free the source array + + add_response_header(h, strdup("Content-type"), strdup("application/json")); + h->response_code = 200; + h->response_string = strdup("OK"); + h->response_body_length = StrLength(sj); + h->response_body = SmashStrBuf(&sj); +} + + +// Dispatcher for paths starting with /ctdl/c/ +void ctdl_c(struct http_transaction *h, struct ctdlsession *c) { + if (!strcasecmp(h->url, "/ctdl/c/info")) { + serv_info(h, c); + } + else { + do_404(h); + } +} diff --git a/webcit-ng/server/ctdlclient.c b/webcit-ng/server/ctdlclient.c new file mode 100644 index 000000000..fa35595e6 --- /dev/null +++ b/webcit-ng/server/ctdlclient.c @@ -0,0 +1,311 @@ +// Functions that handle communication with a Citadel Server +// +// Copyright (c) 1987-2022 by the citadel.org team +// +// This program is open source software. Use, duplication, or +// disclosure are subject to the GNU General Public License v3. + +#include "webcit.h" + +struct ctdlsession *cpool = NULL; // linked list of connections to the Citadel server +pthread_mutex_t cpool_mutex = PTHREAD_MUTEX_INITIALIZER; // Lock it before modifying + + +// Read a specific number of bytes of binary data from the Citadel server. +// Returns the number of bytes read or -1 for error. +int ctdl_read_binary(struct ctdlsession *ctdl, char *buf, int bytes_requested) { + int bytes_read = 0; + int c = 0; + + while (bytes_read < bytes_requested) { + c = read(ctdl->sock, &buf[bytes_read], bytes_requested-bytes_read); + if (c <= 0) { + syslog(LOG_DEBUG, "Socket error or zero-length read"); + return (-1); + } + bytes_read += c; + } + return (bytes_read); +} + + +// Read a newline-terminated line of text from the Citadel server. +// Returns the string length or -1 for error. +int ctdl_readline(struct ctdlsession *ctdl, char *buf, int maxbytes) { + int len = 0; + int c = 0; + + if (buf == NULL) { + return (-1); + } + + while (len < maxbytes) { + c = read(ctdl->sock, &buf[len], 1); + if (c <= 0) { + syslog(LOG_DEBUG, "Socket error or zero-length read"); + return (-1); + } + if (buf[len] == '\n') { + if ((len > 0) && (buf[len - 1] == '\r')) { + --len; + } + buf[len] = 0; + // syslog(LOG_DEBUG, "\033[32m[ %s\033[0m", buf); + return (len); + } + ++len; + } + // syslog(LOG_DEBUG, "\033[32m[ %s\033[0m", buf); + return (len); +} + + +// Read lines of text from the Citadel server until a 000 terminator is received. +// Implemented in terms of ctdl_readline() and is therefore transparent... +// Returns a newly allocated StrBuf or NULL for error. +StrBuf *ctdl_readtextmsg(struct ctdlsession *ctdl) { + char buf[1024]; + StrBuf *sj = NewStrBuf(); + if (!sj) { + return NULL; + } + + while (ctdl_readline(ctdl, buf, sizeof(buf)), strcmp(buf, "000")) { + StrBufAppendPrintf(sj, "%s\n", buf); + } + + return sj; +} + + +// Write to the Citadel server. For now we're just wrapping write() in case we +// need to add anything else later. +ssize_t ctdl_write(struct ctdlsession *ctdl, const void *buf, size_t count) { + return write(ctdl->sock, buf, count); +} + + +// printf() type function to send data to the Citadel Server. +void ctdl_printf(struct ctdlsession *ctdl, const char *format, ...) { + va_list arg_ptr; + StrBuf *Buf = NewStrBuf(); + + va_start(arg_ptr, format); + StrBufVAppendPrintf(Buf, format, arg_ptr); + va_end(arg_ptr); + + // syslog(LOG_DEBUG, "\033[32m] %s\033[0m", ChrPtr(Buf)); + ctdl_write(ctdl, (char *) ChrPtr(Buf), StrLength(Buf)); + ctdl_write(ctdl, "\n", 1); + FreeStrBuf(&Buf); +} + + +// Client side - connect to a unix domain socket +int uds_connectsock(char *sockpath) { + struct sockaddr_un addr; + int s; + + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, sockpath, sizeof addr.sun_path); + + s = socket(AF_UNIX, SOCK_STREAM, 0); + if (s < 0) { + syslog(LOG_WARNING, "Can't create socket [%s]: %s", sockpath, strerror(errno)); + return (-1); + } + + if (connect(s, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + syslog(LOG_WARNING, "Can't connect [%s]: %s", sockpath, strerror(errno)); + close(s); + return (-1); + } + return s; +} + + +// Extract from the headers, the username and password the client is attempting to use. +// This could be HTTP AUTH or it could be in the cookies. +void extract_auth(struct http_transaction *h, char *authbuf, int authbuflen) { + if (authbuf == NULL) { + return; + } + + memset(authbuf, 0, authbuflen); + + char *authheader = header_val(h, "Authorization"); + if (authheader) { + if (!strncasecmp(authheader, "Basic ", 6)) { + safestrncpy(authbuf, &authheader[6], authbuflen); + return; // HTTP-AUTH was found -- stop here + } + } + + char *cookieheader = header_val(h, "Cookie"); + if (cookieheader) { + char *wcauth = strstr(cookieheader, "wcauth="); + if (wcauth) { + safestrncpy(authbuf, &cookieheader[7], authbuflen); + char *semicolon = strchr(authbuf, ';'); + if (semicolon != NULL) { + *semicolon = 0; + } + if (strlen(authbuf) < 3) { // impossibly small + authbuf[0] = 0; + } + return; // Cookie auth was found -- stop here + } + } + // no authorization found in headers ... this is an anonymous session +} + + +// Log in to the Citadel server. Returns 0 on success or nonzero on error. +// +// 'auth' should be a base64-encoded "username:password" combination (like in http-auth) +// +// If 'resultbuf' is not NULL, it should be a buffer of at least 1024 characters, +// and will be filled with the result from a Citadel server command. +int login_to_citadel(struct ctdlsession *c, char *auth, char *resultbuf) { + char localbuf[1024]; + char *buf; + int buflen; + char supplied_username[AUTH_MAX]; + char supplied_password[AUTH_MAX]; + + if (resultbuf != NULL) { + buf = resultbuf; + } + else { + buf = localbuf; + } + + buflen = CtdlDecodeBase64(buf, auth, strlen(auth)); + extract_token(supplied_username, buf, 0, ':', sizeof supplied_username); + extract_token(supplied_password, buf, 1, ':', sizeof supplied_password); + syslog(LOG_DEBUG, "Supplied credentials: username=%s, password=(%d bytes)", supplied_username, (int) strlen(supplied_password)); + + ctdl_printf(c, "USER %s", supplied_username); + ctdl_readline(c, buf, 1024); + if (buf[0] != '3') { + syslog(LOG_DEBUG, "No such user: %s", buf); + return(1); // no such user; resultbuf will explain why + } + + ctdl_printf(c, "PASS %s", supplied_password); + ctdl_readline(c, buf, 1024); + + if (buf[0] == '2') { + extract_token(c->whoami, &buf[4], 0, '|', sizeof c->whoami); + syslog(LOG_DEBUG, "Logged in as %s", c->whoami); + + // Re-encode the auth string so it contains the properly formatted username + char new_auth_string[1024]; + snprintf(new_auth_string, sizeof(new_auth_string), "%s:%s", c->whoami, supplied_password); + CtdlEncodeBase64(c->auth, new_auth_string, strlen(new_auth_string), BASE64_NO_LINEBREAKS); + + return(0); + } + + syslog(LOG_DEBUG, "Login failed: %s", &buf[4]); + return(1); // login failed; resultbuf will explain why +} + + +// This is a variant of the "server connection pool" design pattern. We go through our list +// of connections to Citadel Server, looking for a connection that is at once: +// 1. Not currently serving a WebCit transaction (is_bound) +// 2a. Is logged in to Citadel as the correct user, if the HTTP session is logged in; or +// 2b. Is NOT logged in to Citadel, if the HTTP session is not logged in. +// If we find a qualifying connection, we bind to it for the duration of this WebCit HTTP transaction. +// Otherwise, we create a new connection to Citadel Server and add it to the pool. +struct ctdlsession *connect_to_citadel(struct http_transaction *h) { + struct ctdlsession *cptr = NULL; + struct ctdlsession *my_session = NULL; + int is_new_session = 0; + char buf[1024]; + char auth[AUTH_MAX]; + int r = 0; + + // Does the request carry a username and password? + extract_auth(h, auth, sizeof auth); + + // Lock the connection pool while we claim our connection + pthread_mutex_lock(&cpool_mutex); + if (cpool != NULL) { + for (cptr = cpool; ((cptr != NULL) && (my_session == NULL)); cptr = cptr->next) { + if ((cptr->is_bound == 0) && (!strcmp(cptr->auth, auth))) { + my_session = cptr; + my_session->is_bound = 1; + } + } + } + if (my_session == NULL) { + syslog(LOG_DEBUG, "No qualifying sessions , starting a new one"); + my_session = malloc(sizeof(struct ctdlsession)); + if (my_session != NULL) { + memset(my_session, 0, sizeof(struct ctdlsession)); + is_new_session = 1; + my_session->next = cpool; + cpool = my_session; + my_session->is_bound = 1; + } + } + pthread_mutex_unlock(&cpool_mutex); + if (my_session == NULL) { + return(NULL); // Could not create a new session (yikes!) + } + + if (my_session->sock < 3) { + is_new_session = 1; + } + else { // make sure our Citadel session is still working + int test_conn; + test_conn = ctdl_write(my_session, HKEY("NOOP\n")); + if (test_conn < 5) { + syslog(LOG_DEBUG, "Citadel session is broken , must reconnect"); + close(my_session->sock); + my_session->sock = 0; + is_new_session = 1; + } + else { + test_conn = ctdl_readline(my_session, buf, sizeof(buf)); + if (test_conn < 1) { + syslog(LOG_DEBUG, "Citadel session is broken , must reconnect"); + close(my_session->sock); + my_session->sock = 0; + is_new_session = 1; + } + } + } + + if (is_new_session) { + strcpy(my_session->room, ""); + static char *ctdl_sock_path = NULL; + if (!ctdl_sock_path) { + ctdl_sock_path = malloc(PATH_MAX); + snprintf(ctdl_sock_path, PATH_MAX, "%s/citadel.socket", ctdl_dir); + } + my_session->sock = uds_connectsock(ctdl_sock_path); + ctdl_readline(my_session, buf, sizeof(buf)); // skip past the server greeting banner + + if (!IsEmptyStr(auth)) { // do we need to log in to Citadel? + r = login_to_citadel(my_session, auth, NULL); // FIXME figure out what happens if login failed + } + } + + ctdl_printf(my_session, "NOOP"); + ctdl_readline(my_session, buf, sizeof(buf)); + my_session->last_access = time(NULL); + ++my_session->num_requests_handled; + return(my_session); +} + + +// Release our Citadel Server connection back into the pool. +void disconnect_from_citadel(struct ctdlsession *ctdl) { + pthread_mutex_lock(&cpool_mutex); + ctdl->is_bound = 0; + pthread_mutex_unlock(&cpool_mutex); +} diff --git a/webcit-ng/server/ctdlfunctions.c b/webcit-ng/server/ctdlfunctions.c new file mode 100644 index 000000000..ba25fd637 --- /dev/null +++ b/webcit-ng/server/ctdlfunctions.c @@ -0,0 +1,36 @@ +// +// These utility functions loosely make up a Citadel protocol client library. +// +// Copyright (c) 2016-2022 by the citadel.org team +// +// This program is open source software. Use, duplication, or +// disclosure are subject to the GNU General Public License v3. + +#include "webcit.h" + +// Delete one or more messages from the connected Citadel server. +// This function expects the session to already be "in" the room from which the messages will be deleted. +void ctdl_delete_msgs(struct ctdlsession *c, long *msgnums, int num_msgs) { + int i = 0; + char buf[1024]; + + if ((c == NULL) || (msgnums == NULL) || (num_msgs < 1)) { + return; + } + + i = 0; + strcpy(buf, "DELE "); + do { + sprintf(&buf[strlen(buf)], "%ld", msgnums[i]); + if ((((i + 1) % 50) == 0) || (i == num_msgs - 1)) // delete up to 50 messages with one server command + { + syslog(LOG_DEBUG, "%s", buf); + ctdl_printf(c, "%s", buf); + ctdl_readline(c, buf, sizeof(buf)); + syslog(LOG_DEBUG, "%s", buf); + } + else { + strcat(buf, ","); + } + } while (++i < num_msgs); +} diff --git a/webcit-ng/server/floor_functions.c b/webcit-ng/server/floor_functions.c new file mode 100644 index 000000000..ccb632668 --- /dev/null +++ b/webcit-ng/server/floor_functions.c @@ -0,0 +1,56 @@ +// +// Floor functions +// +// Copyright (c) 1996-2022 by the citadel.org team +// +// This program is open source software. Use, duplication, or +// disclosure are subject to the GNU General Public License v3. + +#include "webcit.h" + + +// Dispatcher for "/ctdl/f" and "/ctdl/f/" for the floor list +void floor_list(struct http_transaction *h, struct ctdlsession *c) { + char buf[1024]; + char floorname[1024]; + + ctdl_printf(c, "LFLR"); + ctdl_readline(c, buf, sizeof(buf)); + if (buf[0] != '1') { + do_502(h); + return; + } + + JsonValue *j = NewJsonArray(HKEY("lflr")); + while (ctdl_readline(c, buf, sizeof(buf)), strcmp(buf, "000")) { + + // 0 |1 |2 + // num|name|refcount + JsonValue *jr = NewJsonObject(HKEY("floor")); + + extract_token(floorname, buf, 1, '|', sizeof floorname); + JsonObjectAppend(jr, NewJsonPlainString(HKEY("name"), floorname, -1)); + + JsonObjectAppend(jr, NewJsonNumber(HKEY("num"), extract_int(buf, 0))); + JsonObjectAppend(jr, NewJsonNumber(HKEY("refcount"), extract_int(buf, 2))); + + JsonArrayAppend(j, jr); // add the room to the array + } + + StrBuf *sj = NewStrBuf(); + SerializeJson(sj, j, 1); // '1' == free the source array + + add_response_header(h, strdup("Content-type"), strdup("application/json")); + h->response_code = 200; + h->response_string = strdup("OK"); + h->response_body_length = StrLength(sj); + h->response_body = SmashStrBuf(&sj); +} + + +// Dispatcher for paths starting with /ctdl/f/ +// (This is a stub ... we will need to add more functions when we can do more than just a floor list) +void ctdl_f(struct http_transaction *h, struct ctdlsession *c) { + floor_list(h, c); + return; +} diff --git a/webcit-ng/server/forum_view.c b/webcit-ng/server/forum_view.c new file mode 100644 index 000000000..90ef33746 --- /dev/null +++ b/webcit-ng/server/forum_view.c @@ -0,0 +1,148 @@ +// The code in here feeds messages out as JSON to the client browser. It is currently being used +// for the forum view, but as we implement other views we'll probably reuse a lot of what's here. +// +// Copyright (c) 1996-2022 by the citadel.org team +// +// This program is open source software. Use, duplication, or +// disclosure are subject to the GNU General Public License v3. + +#include "webcit.h" + +// Commands we need to send to Citadel Server before we begin rendering forum view. +// These are common to flat and threaded views. +void setup_for_forum_view(struct ctdlsession *c) { + char buf[1024]; + ctdl_printf(c, "MSGP text/html|text/plain"); // Declare the MIME types we know how to render + ctdl_readline(c, buf, sizeof(buf)); // Ignore the response + ctdl_printf(c, "MSGP dont_decode"); // Tell the server we will decode base64/etc client-side + ctdl_readline(c, buf, sizeof(buf)); // Ignore the response +} + + +// Fetch a single message and return it in JSON format for client-side rendering +void json_render_one_message(struct http_transaction *h, struct ctdlsession *c, long msgnum) { + StrBuf *raw_msg = NULL; + StrBuf *sanitized_msg = NULL; + char buf[1024]; + char content_transfer_encoding[1024] = { 0 }; + char content_type[1024] = { 0 }; + char datetime[128] = { 0 }; + char author[1024] = { 0 }; + char emailaddr[1024] = { 0 }; + int message_originated_locally = 0; + + setup_for_forum_view(c); + + ctdl_printf(c, "MSG4 %ld", msgnum); + ctdl_readline(c, buf, sizeof(buf)); + if (buf[0] != '1') { + do_404(h); + return; + } + + JsonValue *j = NewJsonObject(HKEY("message")); + JsonObjectAppend(j, NewJsonNumber(HKEY("msgnum"), msgnum)); + + while ((ctdl_readline(c, buf, sizeof(buf)) >= 0) && (strcmp(buf, "text")) && (strcmp(buf, "000"))) { + utf8ify_rfc822_string(&buf[5]); + + // citadel header parsing here + if (!strncasecmp(buf, "from=", 5)) { + safestrncpy(author, &buf[5], sizeof author); + } + else if (!strncasecmp(buf, "rfca=", 5)) { + safestrncpy(emailaddr, &buf[5], sizeof emailaddr); + } + else if (!strncasecmp(buf, "time=", 5)) { + JsonObjectAppend(j, NewJsonNumber(HKEY("time"), atol(&buf[5]))); + } + else if (!strncasecmp(buf, "locl=", 5)) { + message_originated_locally = 1; + } + else if (!strncasecmp(buf, "subj=", 5)) { + JsonObjectAppend(j, NewJsonPlainString(HKEY("subj"), &buf[5], -1)); + } + else if (!strncasecmp(buf, "msgn=", 5)) { + JsonObjectAppend(j, NewJsonPlainString(HKEY("msgn"), &buf[5], -1)); + } + else if (!strncasecmp(buf, "wefw=", 5)) { + JsonObjectAppend(j, NewJsonPlainString(HKEY("wefw"), &buf[5], -1)); + } + } + + if (message_originated_locally) { + JsonObjectAppend(j, NewJsonPlainString(HKEY("from"), author, -1)); + } + else { + JsonObjectAppend(j, NewJsonPlainString(HKEY("from"), emailaddr, -1)); // FIXME do compound address string + } + + if (!strcmp(buf, "text")) { + while ((ctdl_readline(c, buf, sizeof(buf)) >= 0) && (strcmp(buf, "")) && (strcmp(buf, "000"))) { + // rfc822 header parsing here + if (!strncasecmp(buf, "Content-transfer-encoding:", 26)) { + strcpy(content_transfer_encoding, &buf[26]); + striplt(content_transfer_encoding); + } + if (!strncasecmp(buf, "Content-type:", 13)) { + strcpy(content_type, &buf[13]); + striplt(content_type); + } + } + if (!strcmp(buf, "000")) { // if we have an empty message, don't try to read further + raw_msg = NULL; + } + else { + raw_msg = ctdl_readtextmsg(c); + } + } + else { + raw_msg = NULL; + } + + if (raw_msg) { + // These are the encodings we know how to handle. Decode in-place. + + if (!strcasecmp(content_transfer_encoding, "base64")) { + StrBufDecodeBase64(raw_msg); + } + if (!strcasecmp(content_transfer_encoding, "quoted-printable")) { + StrBufDecodeQP(raw_msg); + } + + // At this point, raw_msg contains the decoded message. + // Now run through the renderers we have available. + + if (!strncasecmp(content_type, "text/html", 9)) { + sanitized_msg = html2html("UTF-8", 0, c->room, msgnum, raw_msg); + } + else if (!strncasecmp(content_type, "text/plain", 10)) { + sanitized_msg = text2html("UTF-8", 0, c->room, msgnum, raw_msg); + } + else if (!strncasecmp(content_type, "text/x-citadel-variformat", 25)) { + sanitized_msg = variformat2html(raw_msg); + } + else { + sanitized_msg = NewStrBufPlain(HKEY("No renderer for this content type
")); + syslog(LOG_WARNING, "forum_view: no renderer for content type %s", content_type); + } + FreeStrBuf(&raw_msg); + + // If sanitized_msg is not NULL, we have rendered the message and can output it. + if (sanitized_msg) { + JsonObjectAppend(j, NewJsonString(HKEY("text"), sanitized_msg, NEWJSONSTRING_SMASHBUF)); + } + else { + syslog(LOG_WARNING, "forum_view: message %ld of content type %s converted to NULL", msgnum, content_type); + } + } + + StrBuf *sj = NewStrBuf(); + SerializeJson(sj, j, 1); // '1' == free the source object + + add_response_header(h, strdup("Content-type"), strdup("application/json")); + h->response_code = 200; + h->response_string = strdup("OK"); + h->response_body_length = StrLength(sj); + h->response_body = SmashStrBuf(&sj); +} diff --git a/webcit-ng/server/html2html.c b/webcit-ng/server/html2html.c new file mode 100644 index 000000000..b6dadc35e --- /dev/null +++ b/webcit-ng/server/html2html.c @@ -0,0 +1,635 @@ +// +// Output an HTML message, modifying it slightly to make sure it plays nice +// with the rest of our web framework. +// +// Copyright (c) 2005-2022 by the citadel.org team +// +// This program is open source software. Use, duplication, or +// disclosure are subject to the GNU General Public License v3. + +#include "webcit.h" + + +// Strip surrounding single or double quotes from a string. +void stripquotes(char *s) { + int len; + + if (!s) + return; + + len = strlen(s); + if (len < 2) + return; + + if (((s[0] == '\"') && (s[len - 1] == '\"')) || ((s[0] == '\'') && (s[len - 1] == '\''))) { + s[len - 1] = 0; + strcpy(s, &s[1]); + } +} + + +// Check to see if a META tag has overridden the declared MIME character set. +// +// charset Character set name (left unchanged if we don't do anything) +// meta_http_equiv Content of the "http-equiv" portion of the META tag +// meta_content Content of the "content" portion of the META tag +void extract_charset_from_meta(char *charset, char *meta_http_equiv, char *meta_content) { + char *ptr; + char buf[64]; + + if (!charset) + return; + if (!meta_http_equiv) + return; + if (!meta_content) + return; + + if (strcasecmp(meta_http_equiv, "Content-type")) + return; + + ptr = strchr(meta_content, ';'); + if (!ptr) + return; + + safestrncpy(buf, ++ptr, sizeof buf); + striplt(buf); + if (!strncasecmp(buf, "charset=", 8)) { + strcpy(charset, &buf[8]); + + // The brain-damaged webmail program in Microsoft Exchange declares + // a charset of "unicode" when they really mean "UTF-8". GNU iconv + // treats "unicode" as an alias for "UTF-16" so we have to manually + // fix this here, otherwise messages generated in Exchange webmail + // show up as a big pile of weird characters. + if (!strcasecmp(charset, "unicode")) { + strcpy(charset, "UTF-8"); + } + + // Remove wandering punctuation + if ((ptr = strchr(charset, '\"'))) + *ptr = 0; + striplt(charset); + } +} + + +// Sanitize and enhance an HTML message for display. +// Also convert weird character sets to UTF-8 if necessary. +// Also fixup img src="cid:..." type inline images to fetch the image +StrBuf *html2html(const char *supplied_charset, int treat_as_wiki, char *roomname, long msgnum, StrBuf *Source) { + char buf[SIZ]; + char *msg; + char *ptr; + char *msgstart; + char *msgend; + StrBuf *converted_msg; + int buffer_length = 1; + int line_length = 0; + int content_length = 0; + char new_window[SIZ]; + int brak = 0; + int alevel = 0; + int scriptlevel = 0; + int script_start_pos = (-1); + int i; + int linklen; + char charset[128]; + StrBuf *BodyArea = NULL; + + iconv_t ic = (iconv_t) (-1); + char *ibuf; // Buffer of characters to be converted + char *obuf; // Buffer for converted characters + size_t ibuflen; // Length of input buffer + size_t obuflen; // Length of output buffer + char *osav; // Saved pointer to output buffer + + StrBuf *Target = NewStrBuf(); + if (Target == NULL) { + return (NULL); + } + + safestrncpy(charset, supplied_charset, sizeof charset); + sprintf(new_window, "'); + if ((meta_end != NULL) && (meta_end <= msgend)) { + meta_length = meta_end - meta_start + 1; + meta = malloc(meta_length + 1); + safestrncpy(meta, meta_start, meta_length); + meta[meta_length] = 0; + striplt(meta); + if (!strncasecmp(meta, "HTTP-EQUIV=", 11)) { + meta_http_equiv = strdup(&meta[11]); + spaceptr = strchr(meta_http_equiv, ' '); + if (spaceptr != NULL) { + *spaceptr = 0; + meta_content = strdup(++spaceptr); + if (!strncasecmp(meta_content, "content=", 8)) { + strcpy(meta_content, &meta_content[8]); + stripquotes(meta_http_equiv); + stripquotes(meta_content); + extract_charset_from_meta(charset, meta_http_equiv, meta_content); + } + free(meta_content); + } + free(meta_http_equiv); + } + free(meta); + } + } + + // Any of these tags cause everything up to and including + // the tag to be removed. + if ((!strncasecmp(ptr, "HTML", 4)) + || (!strncasecmp(ptr, "HEAD", 4)) + || (!strncasecmp(ptr, "/HEAD", 5)) + || (!strncasecmp(ptr, "BODY", 4))) { + char *pBody = NULL; + + if (!strncasecmp(ptr, "BODY", 4)) { + pBody = ptr; + } + ptr = strchr(ptr, '>'); + if ((ptr == NULL) || (ptr >= msgend)) + break; + if ((pBody != NULL) && (ptr - pBody > 4)) { + char *src; + char *cid_start, *cid_end; + + *ptr = '\0'; + pBody += 4; + while ((isspace(*pBody)) && (pBody < ptr)) + pBody++; + BodyArea = NewStrBufPlain(NULL, ptr - pBody); + + if (pBody < ptr) { + src = strstr(pBody, "cid:"); + if (src) { + cid_start = src + 4; + cid_end = cid_start; + while ((*cid_end != '"') && !isspace(*cid_end) && (cid_end < ptr)) + cid_end++; + + // copy tag and attributes up to src="cid: + StrBufAppendBufPlain(BodyArea, pBody, src - pBody, 0); + + // add in /webcit/mimepart//CID/ + // trailing / stops dumb URL filters getting excited + StrBufAppendPrintf(BodyArea, "/webcit/mimepart/%ld/", msgnum); + StrBufAppendBufPlain(BodyArea, cid_start, cid_end - cid_start, 0); + + if (ptr - cid_end > 0) + StrBufAppendBufPlain(BodyArea, cid_end + 1, ptr - cid_end, 0); + } + else { + StrBufAppendBufPlain(BodyArea, pBody, ptr - pBody, 0); + } + } + *ptr = '>'; + } + ++ptr; + if ((ptr == NULL) || (ptr >= msgend)) + break; + msgstart = ptr; + } + + // Any of these tags cause everything including and following + // the tag to be removed. + if ((!strncasecmp(ptr, "/HTML", 5)) || (!strncasecmp(ptr, "/BODY", 5))) { + --ptr; + msgend = ptr; + strcpy(ptr, ""); + } + + ++ptr; + } + if (msgstart > msg) { + strcpy(msg, msgstart); + } + + // Now go through the message, parsing tags as necessary. + converted_msg = NewStrBufPlain(NULL, content_length + 8192); + + // Convert foreign character sets to UTF-8 if necessary + if ((strcasecmp(charset, "us-ascii")) + && (strcasecmp(charset, "UTF-8")) + && (strcasecmp(charset, "")) + ) { + syslog(LOG_DEBUG, "Converting %s to UTF-8", charset); + ctdl_iconv_open("UTF-8", charset, &ic); + if (ic == (iconv_t) (-1)) { + syslog(LOG_WARNING, "%s:%d iconv_open() failed: %s", __FILE__, __LINE__, strerror(errno)); + } + } + if (Source == NULL) { + if (ic != (iconv_t) (-1)) { + ibuf = msg; + ibuflen = content_length; + obuflen = content_length + (content_length / 2); + obuf = (char *) malloc(obuflen); + osav = obuf; + iconv(ic, &ibuf, &ibuflen, &obuf, &obuflen); + content_length = content_length + (content_length / 2) - obuflen; + osav[content_length] = 0; + free(msg); + msg = osav; + iconv_close(ic); + } + } + else { + if (ic != (iconv_t) (-1)) { + StrBuf *Buf = NewStrBufPlain(NULL, StrLength(Source) + 8096);; + StrBufConvert(Source, Buf, &ic); + FreeStrBuf(&Buf); + iconv_close(ic); + msg = (char *) ChrPtr(Source); // TODO: get rid of this. + } + } + + // At this point, the message has been stripped down to + // only the content inside the tags, and has + // been converted to UTF-8 if it was originally in a foreign + // character set. The text is also guaranteed to be null + // terminated now. + + if (converted_msg == NULL) { + StrBufAppendPrintf(Target, "Error %d: %s
%s:%d", errno, strerror(errno), __FILE__, __LINE__); + goto BAIL; + } + + if (BodyArea != NULL) { // Any attributes that were declared in the tag + StrBufAppendBufPlain(converted_msg, HKEY("
tag + StrBufAppendBuf(converted_msg, BodyArea, 0); + StrBufAppendBufPlain(converted_msg, HKEY(">"), 0); + } + ptr = msg; + msgend = strchr(msg, 0); + while (ptr < msgend) { + + // Try to sanitize the html of any rogue scripts + if (!strncasecmp(ptr, "')))) { + // open external links to new window + StrBufAppendPrintf(converted_msg, new_window); + ptr = &ptr[8]; + } + else if ((treat_as_wiki) + && (strncasecmp(ptr, "'); + char *src; + // FIXME - handle this situation (maybe someone opened an ') + || (ptr[i] == '[') + || (ptr[i] == ']') + || (ptr[i] == '"') + || (ptr[i] == '\'') + ) + linklen = i; + // entity tag? + if (ptr[i] == '&') { + if ((ptr[i + 2] == ';') || + (ptr[i + 3] == ';') || + (ptr[i + 5] == ';') || (ptr[i + 6] == ';') || (ptr[i + 7] == ';')) + linklen = i; + } + if (linklen > 0) + break; + } + if (linklen > 0) { + char *ltreviewptr; + char *nbspreviewptr; + char linkedchar; + int len; + + len = linklen; + linkedchar = ptr[len]; + ptr[len] = '\0'; + // spot for some subject strings tinymce tends to give us. + ltreviewptr = strchr(ptr, '<'); + if (ltreviewptr != NULL) { + *ltreviewptr = '\0'; + linklen = ltreviewptr - ptr; + } + + nbspreviewptr = strstr(ptr, " "); + if (nbspreviewptr != NULL) { + // nbspreviewptr = '\0'; + linklen = nbspreviewptr - ptr; + } + if (ltreviewptr != 0) + *ltreviewptr = '<'; + + ptr[len] = linkedchar; + + content_length += (32 + linklen); + StrBufAppendPrintf(converted_msg, "%s\"", new_window); + StrBufAppendBufPlain(converted_msg, ptr, linklen, 0); + StrBufAppendPrintf(converted_msg, "\">"); + StrBufAppendBufPlain(converted_msg, ptr, linklen, 0); + ptr += linklen; + StrBufAppendPrintf(converted_msg, ""); + } + } + else { + StrBufAppendBufPlain(converted_msg, ptr, 1, 0); + ptr++; + } + + if ((ptr >= msg) && (ptr <= msgend)) { + // We need to know when we're inside a tag, + // so we don't turn things that look like URL's into + // links, when they're already links - or image sources. + if ((ptr > msg) && (*(ptr - 1) == '<')) { + ++brak; + } + if ((ptr > msg) && (*(ptr - 1) == '>')) { + --brak; + if ((scriptlevel == 0) && (script_start_pos >= 0)) { + StrBufCutRight(converted_msg, StrLength(converted_msg) - script_start_pos); + script_start_pos = (-1); + } + } + if (!strncasecmp(ptr, "", 3)) + --alevel; + } + } + + if (BodyArea != NULL) { + StrBufAppendBufPlain(converted_msg, HKEY("
"), 0); // Close the div where we declared attributes copied + FreeStrBuf(&BodyArea); // from the original tag + } + + // uncomment these two lines to override conversion + // memcpy(converted_msg, msg, content_length); + // output_length = content_length; + + // Output our big pile of markup + StrBufAppendBuf(Target, converted_msg, 0); + + BAIL: // A little trailing vertical whitespace... + StrBufAppendPrintf(Target, "
\n"); + + // Now give back the memory + FreeStrBuf(&converted_msg); + if ((msg != NULL) && (Source == NULL)) + free(msg); + return (Target); +} + + +// Look for URL's embedded in a buffer and make them linkable. We use a +// target window in order to keep the Citadel session in its own window. +void UrlizeText(StrBuf * Target, StrBuf * Source, StrBuf * WrkBuf) { + int len, UrlLen, Offset, TrailerLen; + const char *start, *end, *pos; + + FlushStrBuf(Target); + start = NULL; + len = StrLength(Source); + end = ChrPtr(Source) + len; + for (pos = ChrPtr(Source); (pos < end) && (start == NULL); ++pos) { + if (!strncasecmp(pos, "http://", 7)) + start = pos; + else if (!strncasecmp(pos, "ftp://", 6)) + start = pos; + } + + if (start == NULL) { + StrBufAppendBuf(Target, Source, 0); + return; + } + FlushStrBuf(WrkBuf); + + for (pos = ChrPtr(Source) + len; pos > start; --pos) { + if ((!isprint(*pos)) + || (isspace(*pos)) + || (*pos == '{') + || (*pos == '}') + || (*pos == '|') + || (*pos == '\\') + || (*pos == '^') + || (*pos == '[') + || (*pos == ']') + || (*pos == '`') + || (*pos == '<') + || (*pos == '>') + || (*pos == '(') + || (*pos == ')') + ) { + end = pos; + } + } + + UrlLen = end - start; + StrBufAppendBufPlain(WrkBuf, start, UrlLen, 0); + + Offset = start - ChrPtr(Source); + if (Offset != 0) + StrBufAppendBufPlain(Target, ChrPtr(Source), Offset, 0); + StrBufAppendPrintf(Target, "%ca href=%c%s%c TARGET=%c%s%c%c%s%c/A%c", + LB, QU, ChrPtr(WrkBuf), QU, QU, TARGET, QU, RB, ChrPtr(WrkBuf), LB, RB); + + TrailerLen = StrLength(Source) - (end - ChrPtr(Source)); + if (TrailerLen > 0) + StrBufAppendBufPlain(Target, end, TrailerLen, 0); +} + + +void url(char *buf, size_t bufsize) { + int len, UrlLen, Offset, TrailerLen, outpos; + char *start, *end, *pos; + char urlbuf[SIZ]; + char outbuf[SIZ]; + + start = NULL; + len = strlen(buf); + if (len > bufsize) { + syslog(LOG_WARNING, "URL: content longer than buffer!"); + return; + } + end = buf + len; + for (pos = buf; (pos < end) && (start == NULL); ++pos) { + if (!strncasecmp(pos, "http://", 7)) + start = pos; + if (!strncasecmp(pos, "ftp://", 6)) + start = pos; + } + + if (start == NULL) + return; + + for (pos = buf + len; pos > start; --pos) { + if ((!isprint(*pos)) + || (isspace(*pos)) + || (*pos == '{') + || (*pos == '}') + || (*pos == '|') + || (*pos == '\\') + || (*pos == '^') + || (*pos == '[') + || (*pos == ']') + || (*pos == '`') + || (*pos == '<') + || (*pos == '>') + || (*pos == '(') + || (*pos == ')') + ) { + end = pos; + } + } + + UrlLen = end - start; + if (UrlLen > sizeof(urlbuf)) { + syslog(LOG_WARNING, "URL: content longer than buffer!"); + return; + } + memcpy(urlbuf, start, UrlLen); + urlbuf[UrlLen] = '\0'; + + Offset = start - buf; + if ((Offset != 0) && (Offset < sizeof(outbuf))) + memcpy(outbuf, buf, Offset); + outpos = snprintf(&outbuf[Offset], sizeof(outbuf) - Offset, + "%ca href=%c%s%c TARGET=%c%s%c%c%s%c/A%c", LB, QU, urlbuf, QU, QU, TARGET, QU, RB, urlbuf, LB, RB); + if (outpos >= sizeof(outbuf) - Offset) { + syslog(LOG_WARNING, "URL: content longer than buffer!"); + return; + } + + TrailerLen = len - (end - start); + if (TrailerLen > 0) + memcpy(outbuf + Offset + outpos, end, TrailerLen); + if (Offset + outpos + TrailerLen > bufsize) { + syslog(LOG_WARNING, "URL: content longer than buffer!"); + return; + } + memcpy(buf, outbuf, Offset + outpos + TrailerLen); + *(buf + Offset + outpos + TrailerLen) = '\0'; +} diff --git a/webcit-ng/server/http.c b/webcit-ng/server/http.c new file mode 100644 index 000000000..55c335eff --- /dev/null +++ b/webcit-ng/server/http.c @@ -0,0 +1,327 @@ +// This module handles HTTP transactions. +// +// Copyright (c) 1996-2022 by the citadel.org team +// +// This program is open source software. Use, duplication, or +// disclosure are subject to the GNU General Public License v3. + +#include "webcit.h" + +// Write data to the HTTP client. Encrypt if necessary. +int client_write(struct client_handle *ch, char *buf, int nbytes) { + if (is_https) { + return client_write_ssl(ch, buf, nbytes); + } + else { + return write(ch->sock, buf, nbytes); + } +} + + +// Read data from the HTTP client. Decrypt if necessary. +// Returns number of bytes read, or -1 to indicate an error. +int client_read(struct client_handle *ch, char *buf, int nbytes) { + if (is_https) { + return client_read_ssl(ch, buf, nbytes); + } + else { + int bytes_received = 0; + int bytes_this_block = 0; + while (bytes_received < nbytes) { + bytes_this_block = read(ch->sock, &buf[bytes_received], nbytes - bytes_received); + if (bytes_this_block < 1) { + return (-1); + } + bytes_received += bytes_this_block; + } + return (nbytes); + } +} + + +// Read a newline-terminated line of text from the client. +// Implemented in terms of client_read() and is therefore transparent... +// Returns the string length or -1 for error. +int client_readline(struct client_handle *ch, char *buf, int maxbytes) { + int len = 0; + int c = 0; + + if (buf == NULL) { + return (-1); + } + + while (len < maxbytes) { + c = client_read(ch, &buf[len], 1); + if (c <= 0) { + syslog(LOG_DEBUG, "Socket error or zero-length read"); + return (-1); + } + if (buf[len] == '\n') { + if ((len > 0) && (buf[len - 1] == '\r')) { + --len; + } + buf[len] = 0; + return (len); + } + ++len; + } + return (len); +} + + +// printf() type function to send data to the web client. +void client_printf(struct client_handle *ch, const char *format, ...) { + va_list arg_ptr; + StrBuf *Buf = NewStrBuf(); + + va_start(arg_ptr, format); + StrBufVAppendPrintf(Buf, format, arg_ptr); + va_end(arg_ptr); + + client_write(ch, (char *) ChrPtr(Buf), StrLength(Buf)); + FreeStrBuf(&Buf); +} + + +// Push one new header into the response of an HTTP request. +// When completed, the HTTP transaction now owns the memory allocated for key and val. +void add_response_header(struct http_transaction *h, char *key, char *val) { + struct keyval new_response_header; + new_response_header.key = key; + new_response_header.val = val; + array_append(h->response_headers, &new_response_header); +} + + +// Entry point for this layer. Socket is connected. If running as an HTTPS +// server, SSL has already been negotiated. Now perform one transaction. +void perform_one_http_transaction(struct client_handle *ch) { + char buf[1024]; + int len; + int lines_read = 0; + struct http_transaction h; + char *c, *d; + struct sockaddr_in sin; + int i; // general purpose iterator variable + + memset(&h, 0, sizeof h); + h.request_headers = array_new(sizeof(struct keyval)); + h.request_parms = array_new(sizeof(struct keyval)); + h.response_headers = array_new(sizeof(struct keyval)); + + while (len = client_readline(ch, buf, sizeof buf), (len > 0)) { + ++lines_read; + + if (lines_read == 1) { // First line is method and URL. + c = strchr(buf, ' '); // IGnore the HTTP-version. + if (c == NULL) { + h.method = strdup("NULL"); + h.url = strdup("/"); + } + else { + *c = 0; + h.method = strdup(buf); + ++c; + d = c; + c = strchr(d, ' '); + if (c != NULL) { + *c = 0; + } + ++c; + h.url = strdup(d); + } + } + else { // Subsequent lines are headers. + c = strchr(buf, ':'); // Header line folding is obsolete so we don't support it. + if (c != NULL) { + + struct keyval new_request_header; + *c = 0; + new_request_header.key = strdup(buf); + ++c; + new_request_header.val = strdup(c); + striplt(new_request_header.key); + striplt(new_request_header.val); + array_append(h.request_headers, &new_request_header); +#ifdef DEBUG_HTTP + syslog(LOG_DEBUG, "\033[1m\033[35m{ %s: %s\033[0m", new_request_header.key, new_request_header.val); +#endif + } + } + + } + + // If the URL had any query parameters in it, parse them out now. + char *p = (h.url ? strchr(h.url, '?') : NULL); + if (p) { + *p++ = 0; // insert a null to remove parameters from the URL + char *tok, *saveptr = NULL; + for (tok = strtok_r(p, "&", &saveptr); tok!=NULL; tok = strtok_r(NULL, "&", &saveptr)) { + char *eq = strchr(tok, '='); + if (eq) { + *eq++ = 0; + unescape_input(eq); + struct keyval kv; + kv.key = strdup(tok); + kv.val = strdup(eq); + array_append(h.request_parms, &kv); +#ifdef DEBUG_HTTP + syslog(LOG_DEBUG, "\033[1m\033[33m| %s = %s\033[0m", kv.key, kv.val); +#endif + } + } + } + + // build up the site prefix, such as https://foo.bar.com:4343 + h.site_prefix = malloc(256); + strcpy(h.site_prefix, (is_https ? "https://" : "http://")); + char *hostheader = header_val(&h, "Host"); + if (hostheader) { + strcat(h.site_prefix, hostheader); + } + else { + strcat(h.site_prefix, "127.0.0.1"); + } + socklen_t llen = sizeof(sin); + if (getsockname(ch->sock, (struct sockaddr *) &sin, &llen) >= 0) { + sprintf(&h.site_prefix[strlen(h.site_prefix)], ":%d", ntohs(sin.sin_port)); + } + + // Now try to read in the request body (if one is present) + if (len < 0) { + syslog(LOG_DEBUG, "Client disconnected"); + } + else { +//#ifdef DEBUG_HTTP + syslog(LOG_DEBUG, "\033[33m\033[1m< %s %s\033[0m", h.method, h.url); +//#endif + + // If there is a request body, read it now. + char *ccl = header_val(&h, "Content-Length"); + if (ccl) { + h.request_body_length = atol(ccl); + } + if (h.request_body_length > 0) { + syslog(LOG_DEBUG, "Reading request body (%ld bytes)", h.request_body_length); + h.request_body = malloc(h.request_body_length); + client_read(ch, h.request_body, h.request_body_length); + + // Write the entire request body to stderr -- not what you want during normal operation. + #ifdef BODY_TO_STDERR + write(2, HKEY("\033[31m")); + write(2, h.request_body, h.request_body_length); + write(2, HKEY("\033[0m\n")); + #endif + + } + + // Now pass control up to the next layer to perform the request. + perform_request(&h); + + // Write the entire response body to stderr -- not what you want during normal operation. + #ifdef BODY_TO_STDERR + write(2, HKEY("\033[32m")); + write(2, h.response_body, h.response_body_length); + write(2, HKEY("\033[0m\n")); + #endif + + // Output the results back to the client. +#ifdef DEBUG_HTTP + syslog(LOG_DEBUG, "\033[33m\033[1m> %03d %s\033[0m", h.response_code, h.response_string); +#endif + client_printf(ch, "HTTP/1.1 %03d %s\r\n", h.response_code, h.response_string); + client_printf(ch, "Connection: close\r\n"); + client_printf(ch, "Content-Length: %ld\r\n", h.response_body_length); + char *datestring = http_datestring(time(NULL)); + if (datestring) { + client_printf(ch, "Date: %s\r\n", datestring); + free(datestring); + } + + client_printf(ch, "Content-encoding: identity\r\n"); // change if we gzip/deflate + int number_of_response_headers = array_len(h.response_headers); + for (i=0; ikey, kv->val); +#endif + client_printf(ch, "%s: %s\r\n", kv->key, kv->val); + } + client_printf(ch, "\r\n"); + if ((h.response_body_length > 0) && (h.response_body != NULL)) { + client_write(ch, h.response_body, h.response_body_length); + } + } + + // free the transaction memory + while (array_len(h.request_headers) > 0) { + struct keyval *kv = array_get_element_at(h.request_headers, 0); + if (kv->key) free(kv->key); + if (kv->val) free(kv->val); + array_delete_element_at(h.request_headers, 0); + } + array_free(h.request_headers); + while (array_len(h.request_parms) > 0) { + struct keyval *kv = array_get_element_at(h.request_parms, 0); + if (kv->key) free(kv->key); + if (kv->val) free(kv->val); + array_delete_element_at(h.request_parms, 0); + } + array_free(h.request_parms); + while (array_len(h.response_headers) > 0) { + struct keyval *kv = array_get_element_at(h.response_headers, 0); + if (kv->key) free(kv->key); + if (kv->val) free(kv->val); + array_delete_element_at(h.response_headers, 0); + } + array_free(h.response_headers); + if (h.method) { + free(h.method); + } + if (h.url) { + free(h.url); + } + if (h.request_body) { + free(h.request_body); + } + if (h.response_string) { + free(h.response_string); + } + if (h.site_prefix) { + free(h.site_prefix); + } +} + + +// Utility function to fetch a specific header from a completely read-in request. +// Returns the value of the requested header, or NULL if the specified header was not sent. +// The caller does NOT own the memory of the returned pointer, but can count on the pointer +// to still be valid through the end of the transaction. +char *header_val(struct http_transaction *h, char *requested_header) { + struct keyval *kv; + int i; + for (i=0; irequest_headers); ++i) { + kv = array_get_element_at(h->request_headers, i); + if (!strcasecmp(kv->key, requested_header)) { + return (kv->val); + } + } + return (NULL); +} + + +// Utility function to fetch a specific URL parameter from a completely read-in request. +// Returns the value of the requested parameter, or NULL if the specified parameter was not sent. +// The caller does NOT own the memory of the returned pointer, but can count on the pointer +// to still be valid through the end of the transaction. +char *get_url_param(struct http_transaction *h, char *requested_param) { + struct keyval *kv; + int i; + for (i=0; irequest_parms); ++i) { + kv = array_get_element_at(h->request_parms, i); + if (!strcasecmp(kv->key, requested_param)) { + return (kv->val); + } + } + return (NULL); +} diff --git a/webcit-ng/server/main.c b/webcit-ng/server/main.c new file mode 100644 index 000000000..031369131 --- /dev/null +++ b/webcit-ng/server/main.c @@ -0,0 +1,129 @@ +// Main entry point for the program. +// +// Copyright (c) 1996-2022 by the citadel.org team +// +// This program is open source software. Use, duplication, or +// disclosure are subject to the GNU General Public License v3. + +#include "webcit.h" // All other headers are included from this header. + +char *ctdl_dir = CTDL_DIR; + +// Main entry point for the web server. +int main(int argc, char **argv) { + int webserver_port = WEBSERVER_PORT; + char *webserver_interface = WEBSERVER_INTERFACE; + int running_as_daemon = 0; + int webserver_protocol = WEBSERVER_HTTP; + int a; + char *pid_file = NULL; + + // Parse command line + while ((a = getopt(argc, argv, "u:h:i:p:t:T:B:x:g:dD:G:cfsS:Z:v:")) != EOF) + switch (a) { + case 'u': + setuid(atoi(optarg)); + break; + case 'h': + ctdl_dir = strdup(optarg); + break; + case 'd': + running_as_daemon = 1; + break; + case 'D': + running_as_daemon = 1; + pid_file = strdup(optarg); + break; + case 'g': + // FIXME set up the default landing page + break; + case 'B': + case 't': + case 'x': + case 'T': + case 'v': + // The above options are no longer used, but ignored so old scripts don't break + break; + case 'i': + webserver_interface = optarg; + break; + case 'p': + webserver_port = atoi(optarg); + break; + case 'Z': + // FIXME when gzip is added, disable it if this flag is set + break; + case 'f': + //follow_xff = 1; + break; + case 'c': + break; + case 's': + is_https = 1; + webserver_protocol = WEBSERVER_HTTPS; + break; + case 'S': + is_https = 1; + webserver_protocol = WEBSERVER_HTTPS; + //ssl_cipher_list = strdup(optarg); + break; + case 'G': + //DumpTemplateI18NStrings = 1; + //I18nDump = NewStrBufPlain(HKEY("int templatestrings(void)\n{\n")); + //I18nDumpFile = optarg; + break; + default: + fprintf(stderr, "usage:\nwebcit " + "[-i ip_addr] [-p http_port] " + "[-c] [-f] " + "[-d] [-Z] [-G i18ndumpfile] " + "[-u uid] [-h homedirectory] " + "[-D daemonizepid] [-v] " + "[-g defaultlandingpage] [-B basesize] " + "[-s] [-S cipher_suites]" + "[-h citadel_server_directory]\n" + ); + return 1; + } + + while (optind < argc) { + ctdl_dir = strdup(argv[optind++]); + } + + // Start the logger + openlog("webcit", (running_as_daemon ? (LOG_PID) : (LOG_PID | LOG_PERROR)), LOG_DAEMON); + + // Tell 'em who's in da house + syslog(LOG_NOTICE, "MAKE WEBCIT GREAT AGAIN!"); + syslog(LOG_NOTICE, "Copyright (C) 1996-2022 by the citadel.org team"); + syslog(LOG_NOTICE, " "); + syslog(LOG_NOTICE, "This program is open source software: you can redistribute it and/or"); + syslog(LOG_NOTICE, "modify it under the terms of the GNU General Public License, version 3."); + syslog(LOG_NOTICE, " "); + syslog(LOG_NOTICE, "This program is distributed in the hope that it will be useful,"); + syslog(LOG_NOTICE, "but WITHOUT ANY WARRANTY; without even the implied warranty of"); + syslog(LOG_NOTICE, "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the"); + syslog(LOG_NOTICE, "GNU General Public License for more details."); + syslog(LOG_NOTICE, " "); + + // Ensure that we are linked to the correct version of libcitadel + if (libcitadel_version_number() < LIBCITADEL_VERSION_NUMBER) { + syslog(LOG_INFO, " You are running libcitadel version %d", libcitadel_version_number()); + syslog(LOG_INFO, "WebCit was compiled against version %d", LIBCITADEL_VERSION_NUMBER); + return (1); + } + + // Go into the background if we were asked to run as a daemon + if (running_as_daemon) { + daemon(1, 0); + if (pid_file != NULL) { + FILE *pfp = fopen(pid_file, "w"); + if (pfp) { + fprintf(pfp, "%d\n", getpid()); + fclose(pfp); + } + } + } + + return webserver(webserver_interface, webserver_port, webserver_protocol); +} diff --git a/webcit-ng/server/messages.c b/webcit-ng/server/messages.c new file mode 100644 index 000000000..1ef039f82 --- /dev/null +++ b/webcit-ng/server/messages.c @@ -0,0 +1,264 @@ +// +// Message base functions +// +// Copyright (c) 1996-2022 by the citadel.org team +// +// This program is open source software. Use, duplication, or +// disclosure are subject to the GNU General Public License v3. + +#include "webcit.h" + + +// Given an encoded UID, translate that to an unencoded Citadel EUID and +// then search for it in the current room. Return a message number or -1 +// if not found. +long locate_message_by_uid(struct ctdlsession *c, char *uid) { + char buf[1024]; + + ctdl_printf(c, "EUID %s", uid); + ctdl_readline(c, buf, sizeof buf); + if (buf[0] == '2') { + return (atol(&buf[4])); + + } + + // Ugly hack to handle Mozilla Thunderbird, try stripping ".ics" if present + if (!strcasecmp(&uid[strlen(uid) - 4], ".ics")) { + safestrncpy(buf, uid, sizeof buf); + buf[strlen(buf) - 4] = 0; + ctdl_printf(c, "EUID %s", buf); + ctdl_readline(c, buf, sizeof buf); + if (buf[0] == '2') { + return (atol(&buf[4])); + + } + } + + return (-1); +} + + +// DAV delete an object in a room. +void dav_delete_message(struct http_transaction *h, struct ctdlsession *c, long msgnum) { + ctdl_delete_msgs(c, &msgnum, 1); + h->response_code = 204; + h->response_string = strdup("no content"); +} + + +// GET method directly on a message in a room +void dav_get_message(struct http_transaction *h, struct ctdlsession *c, long msgnum) { + char buf[1024]; + int in_body = 0; + int encoding = 0; + StrBuf *Body = NULL; + + ctdl_printf(c, "MSG2 %ld", msgnum); + ctdl_readline(c, buf, sizeof buf); + if (buf[0] != '1') { + do_404(h); + return; + } + + char *etag = malloc(20); + if (etag != NULL) { + sprintf(etag, "%ld", msgnum); + add_response_header(h, strdup("ETag"), etag); // http_transaction now owns this memory + } + + while (ctdl_readline(c, buf, sizeof buf), strcmp(buf, "000")) { + if (IsEmptyStr(buf) && (in_body == 0)) { + in_body = 1; + Body = NewStrBuf(); + } + else if (in_body == 0) { + char *k = buf; + char *v = strchr(buf, ':'); + if (v) { + *v = 0; + ++v; + striplt(v); // we now have a key (k) and a value (v) + if ((!strcasecmp(k, "content-type")) // fields which can be passed from RFC822 to HTTP as-is + || (!strcasecmp(k, "date")) + ) { + add_response_header(h, strdup(k), strdup(v)); + } + else if (!strcasecmp(k, "content-transfer-encoding")) { + if (!strcasecmp(v, "base64")) { + encoding = 'b'; + } + else if (!strcasecmp(v, "quoted-printable")) { + encoding = 'q'; + } + } + } + } + else if ((in_body == 1) && (Body != NULL)) { + StrBufAppendPrintf(Body, "%s\n", buf); + } + } + + h->response_code = 200; + h->response_string = strdup("OK"); + + if (Body != NULL) { + if (encoding == 'q') { + h->response_body = malloc(StrLength(Body)); + if (h->response_body != NULL) { + h->response_body_length = + CtdlDecodeQuotedPrintable(h->response_body, (char *) ChrPtr(Body), StrLength(Body)); + } + FreeStrBuf(&Body); + } + else if (encoding == 'b') { + h->response_body = malloc(StrLength(Body)); + if (h->response_body != NULL) { + h->response_body_length = CtdlDecodeBase64(h->response_body, ChrPtr(Body), StrLength(Body)); + } + FreeStrBuf(&Body); + } + else { + h->response_body_length = StrLength(Body); + h->response_body = SmashStrBuf(&Body); + } + } +} + + +// PUT a message into a room +void dav_put_message(struct http_transaction *h, struct ctdlsession *c, char *euid, long old_msgnum) { + char buf[1024]; + char *content_type = NULL; + int n; + long new_msgnum; + char new_euid[1024]; + char response_string[1024]; + + if ((h->request_body == NULL) || (h->request_body_length < 1)) { + do_404(h); // Refuse to post a null message + return; + } + + char *wefw = get_url_param(h, "wefw"); // references + if (!wefw) wefw = ""; + char *subj = get_url_param(h, "subj"); // subject + if (!subj) subj = ""; + + // Mode 4 will give us metadata back after upload + ctdl_printf(c, "ENT0 1|||4|%s||1|||||%s|", subj, wefw); + ctdl_readline(c, buf, sizeof buf); + if (buf[0] != '8') { + h->response_code = 502; + h->response_string = strdup("bad gateway"); + add_response_header(h, strdup("Content-type"), strdup("text/plain")); + h->response_body = strdup(buf); + h->response_body_length = strlen(h->response_body); + return; + } + + // Remember, ctdl_printf() appends \n on its own, so when adding a CRLF newline, only use \r + // Or for a blank line, use ctdl_write() with \r\n + + content_type = header_val(h, "Content-type"); + ctdl_printf(c, "Content-type: %s\r", (content_type ? content_type : "application/octet-stream")); + ctdl_write(c, HKEY("\r\n")); + ctdl_write(c, h->request_body, h->request_body_length); + if (h->request_body[h->request_body_length] != '\n') { + ctdl_write(c, HKEY("\r\n")); + } + ctdl_printf(c, "000"); + + // Now handle the response from the Citadel server. + + n = 0; + new_msgnum = 0; + strcpy(new_euid, ""); + strcpy(response_string, ""); + + while (ctdl_readline(c, buf, sizeof buf), strcmp(buf, "000")) + switch (n++) { + case 0: + new_msgnum = atol(buf); + break; + case 1: + safestrncpy(response_string, buf, sizeof response_string); + syslog(LOG_DEBUG, "new_msgnum=%ld (%s)\n", new_msgnum, buf); + break; + case 2: + safestrncpy(new_euid, buf, sizeof new_euid); + break; + default: + break; + } + + // Tell the client what happened. + + // Citadel failed in some way? + char *new_location = malloc(1024); + if ((new_msgnum < 0L) || (new_location == NULL)) { + h->response_code = 502; + h->response_string = strdup("bad gateway"); + add_response_header(h, strdup("Content-type"), strdup("text/plain")); + h->response_body = strdup(response_string); + h->response_body_length = strlen(h->response_body); + return; + } + + char *etag = malloc(20); + if (etag != NULL) { + sprintf(etag, "%ld", new_msgnum); + add_response_header(h, strdup("ETag"), etag); // http_transaction now owns this memory + } + + char esc_room[1024]; + char esc_euid[1024]; + urlesc(esc_room, sizeof esc_room, c->room); + urlesc(esc_euid, sizeof esc_euid, new_euid); + snprintf(new_location, 1024, "/ctdl/r/%s/%s", esc_room, esc_euid); + add_response_header(h, strdup("Location"), new_location); // http_transaction now owns this memory + + if (old_msgnum <= 0) { + h->response_code = 201; // We created this item for the first time. + h->response_string = strdup("created"); + } + else { + h->response_code = 204; // We modified an existing item. + h->response_string = strdup("no content"); + + // The item we replaced has probably already been deleted by + // the Citadel server, but we'll do this anyway, just in case. + ctdl_delete_msgs(c, &old_msgnum, 1); + } + +} + + +// Download a single component of a MIME-encoded message +void download_mime_component(struct http_transaction *h, struct ctdlsession *c, long msgnum, char *partnum) { + char buf[1024]; + char content_type[1024]; + + ctdl_printf(c, "DLAT %ld|%s", msgnum, partnum); + ctdl_readline(c, buf, sizeof buf); + if (buf[0] != '6') { + do_404(h); // too bad, so sad, go away + } + // Server response is going to be: 6XX length|-1|filename|content-type|charset + h->response_body_length = extract_int(&buf[4], 0); + extract_token(content_type, buf, 3, '|', sizeof content_type); + + h->response_body = malloc(h->response_body_length + 1); + int bytes = 0; + int thisblock; + do { + thisblock = read(c->sock, &h->response_body[bytes], (h->response_body_length - bytes)); + bytes += thisblock; + syslog(LOG_DEBUG, "Bytes read: %d of %d", (int) bytes, (int) h->response_body_length); + } while ((bytes < h->response_body_length) && (thisblock >= 0)); + h->response_body[h->response_body_length] = 0; // null terminate it just for good measure + syslog(LOG_DEBUG, "content type: %s", content_type); + + add_response_header(h, strdup("Content-type"), strdup(content_type)); + h->response_code = 200; + h->response_string = strdup("OK"); +} diff --git a/webcit-ng/server/request.c b/webcit-ng/server/request.c new file mode 100644 index 000000000..605520e68 --- /dev/null +++ b/webcit-ng/server/request.c @@ -0,0 +1,183 @@ +// This module sits directly above the HTTP layer. By the time we get here, +// an HTTP request has been fully received and parsed. Control is passed up +// to this layer to actually perform the request. We then fill in the response +// and pass control back down to the HTTP layer to output the response back to +// the client. +// +// Copyright (c) 1996-2022 by the citadel.org team +// +// This program is open source software. Use, duplication, or +// disclosure are subject to the GNU General Public License v3. + +#include "webcit.h" + + +// Not found! Wowzers. +void do_404(struct http_transaction *h) { + h->response_code = 404; + h->response_string = strdup("NOT FOUND"); + add_response_header(h, strdup("Content-type"), strdup("text/plain")); + h->response_body = strdup("404 NOT FOUND\r\n"); + h->response_body_length = strlen(h->response_body); +} + + +// Precondition failed (such as if-match) +void do_412(struct http_transaction *h) { + h->response_code = 412; + h->response_string = strdup("PRECONDITION FAILED"); +} + + +// Succeed with no output +void do_204(struct http_transaction *h) { + h->response_code = 204; + h->response_string = strdup("No content"); +} + + +// We throw an HTTP error "502 bad gateway" when we need to connect to Citadel, but can't. +void do_502(struct http_transaction *h) { + h->response_code = 502; + h->response_string = strdup("bad gateway"); + add_response_header(h, strdup("Content-type"), strdup("text/plain")); + h->response_body = strdup(_("This program was unable to connect or stay connected to the Citadel server. Please report this problem to your system administrator.")); + h->response_body_length = strlen(h->response_body); +} + + +// Tell the client to authenticate using HTTP-AUTH (RFC 2617) +void request_http_authenticate(struct http_transaction *h) { + h->response_code = 401; + h->response_string = strdup("Unauthorized"); + add_response_header(h, strdup("WWW-Authenticate"), strdup("Basic realm=\"Citadel Server\"")); +} + + +// Easy and fun utility function to throw a redirect. +void http_redirect(struct http_transaction *h, char *to_where) { + syslog(LOG_DEBUG, "Redirecting to: %s", to_where); + h->response_code = 302; + h->response_string = strdup("Moved Temporarily"); + add_response_header(h, strdup("Location"), strdup(to_where)); + add_response_header(h, strdup("Content-type"), strdup("text/plain")); + h->response_body = strdup(to_where); + h->response_body_length = strlen(h->response_body); +} + + +// perform_request() is the entry point for *every* HTTP transaction. +void perform_request(struct http_transaction *h) { + struct ctdlsession *c; + + // Determine which code path to take based on the beginning of the URL. + // This is implemented as a series of strncasecmp() calls rather than a + // lookup table in order to make the code more readable. + + if (IsEmptyStr(h->url)) { // Sanity check + do_404(h); + return; + } + + // Right about here is where we should try to handle anything that doesn't start + // with the /ctdl/ prefix. + // Root (/) ... + + if ((!strcmp(h->url, "/")) && (!strcasecmp(h->method, "GET"))) { + http_redirect(h, "/ctdl/s/index.html"); + return; + } + + // Legacy URL patterns (/readnew?gotoroom=xxx&start_reading_at=yyy) ... + // Direct room name (/my%20blog) ... + + // HTTP-01 challenge [RFC5785 section 3, RFC8555 section 9.2] + if (!strncasecmp(h->url, HKEY("/.well-known"))) { // Static content + output_static(h); + return; + } + + if (!strcasecmp(h->url, "/favicon.ico")) { + output_static(h); + return; + } + + // Everything below this line is strictly REST URL patterns. + + if (strncasecmp(h->url, HKEY("/ctdl/"))) { // Reject non-REST + do_404(h); + return; + } + + if (!strncasecmp(h->url, HKEY("/ctdl/s/"))) { // Static content + output_static(h); + return; + } + + if (h->url[7] != '/') { + do_404(h); + return; + } + + // Anything below this line: + // 1. Is in the format of /ctdl/?/* + // 2. Requires a connection to a Citadel server. + + c = connect_to_citadel(h); + if (c == NULL) { + do_502(h); + return; + } + + // WebDAV methods like OPTIONS and PROPFIND *require* a logged-in session, + // even if the Citadel server allows anonymous access. + if (IsEmptyStr(c->auth)) { + if ( (!strcasecmp(h->method, "OPTIONS")) + || (!strcasecmp(h->method, "PROPFIND")) + || (!strcasecmp(h->method, "REPORT")) + || (!strcasecmp(h->method, "DELETE")) + ) { + request_http_authenticate(h); + disconnect_from_citadel(c); + return; + } + } + + // Break down the URL by path and send the request to the appropriate part of the program. + switch (h->url[6]) { + case 'a': // /ctdl/a/ == RESTful path to admin functions + ctdl_a(h, c); + break; + case 'c': // /ctdl/c/ == misc Citadel server commands + ctdl_c(h, c); + break; + case 'f': // /ctdl/f/ == RESTful path to floors + ctdl_f(h, c); + break; + case 'r': // /ctdl/r/ == RESTful path to rooms + ctdl_r(h, c); + break; + case 'u': // /ctdl/u/ == RESTful path to users + ctdl_u(h, c); + break; + default: + do_404(h); + } + + // Are we in an authenticated session? If so, set a cookie so we stay logged in. + if (!IsEmptyStr(c->auth)) { + char koekje[AUTH_MAX]; + char *exp = http_datestring(time(NULL) + (86400 * 30)); + snprintf(koekje, AUTH_MAX, "wcauth=%s; path=/ctdl/; Expires=%s", c->auth, exp); // warn + free(exp); + add_response_header(h, strdup("Set-Cookie"), strdup(koekje)); + } + + // Durlng development we are foiling the browser cache completely. In production we'll be more selective. + add_response_header(h, strdup("Cache-Control"), strdup("no-store, must-revalidate")); + add_response_header(h, strdup("Pragma"), strdup("no-cache")); + add_response_header(h, strdup("Expires"), strdup("0")); + + // Unbind from our Citadel server connection. + disconnect_from_citadel(c); +} diff --git a/webcit-ng/server/room_functions.c b/webcit-ng/server/room_functions.c new file mode 100644 index 000000000..67b3e9082 --- /dev/null +++ b/webcit-ng/server/room_functions.c @@ -0,0 +1,679 @@ +// Room functions +// +// Copyright (c) 1996-2022 by the citadel.org team +// +// This program is open source software. Use, duplication, or +// disclosure are subject to the GNU General Public License v3. + +#include "webcit.h" + + +// Return a "zero-terminated" array of message numbers in the current room. +// Caller owns the memory and must free it. Returns NULL if any problems. +long *get_msglist(struct ctdlsession *c, char *which_msgs) { + char buf[1024]; + long *msglist = NULL; + int num_msgs = 0; + int num_alloc = 0; + + ctdl_printf(c, "MSGS %s", which_msgs); + ctdl_readline(c, buf, sizeof(buf)); + if (buf[0] == '1') { + do { + if (num_msgs >= num_alloc) { + if (num_alloc == 0) { + num_alloc = 1024; + msglist = malloc(num_alloc * sizeof(long)); + } + else { + num_alloc *= 2; + msglist = realloc(msglist, num_alloc * sizeof(long)); + } + } + ctdl_readline(c, buf, sizeof(buf)); + msglist[num_msgs++] = atol(buf); + } while (strcmp(buf, "000")); // this makes the last element a "0" terminator + } + return msglist; +} + + +// Supplied with a list of potential matches from an If-Match: or If-None-Match: header, and +// a message number (which we always use as the entity tag in Citadel), return nonzero if the +// message number matches any of the supplied tags in the string. +int match_etags(char *taglist, long msgnum) { + int num_tags = num_tokens(taglist, ','); + int i = 0; + char tag[1024]; + + if (msgnum <= 0) { // no msgnum? no match. + return (0); + } + + for (i = 0; i < num_tags; ++i) { + extract_token(tag, taglist, i, ',', sizeof tag); + striplt(tag); + char *lq = (strchr(tag, '"')); + char *rq = (strrchr(tag, '"')); + if (lq < rq) { // has two double quotes + strcpy(rq, ""); + strcpy(tag, ++lq); + } + striplt(tag); + if (!strcmp(tag, "*")) { // wildcard match + return (1); + } + long tagmsgnum = atol(tag); + if ((tagmsgnum > 0) && (tagmsgnum == msgnum)) { // match + return (1); + } + } + + return (0); // no match +} + + +// Client is requesting a STAT (name and modification time) of the current room +void json_stat(struct http_transaction *h, struct ctdlsession *c) { + char buf[1024]; + char field[1024]; + + ctdl_printf(c, "STAT"); + ctdl_readline(c, buf, sizeof(buf)); + if (buf[0] == '2') { + JsonValue *j = NewJsonObject(HKEY("stat")); + extract_token(field, &buf[4], 0, '|', sizeof field); + JsonObjectAppend(j, NewJsonPlainString(HKEY("name"), field, -1)); + JsonObjectAppend(j, NewJsonNumber(HKEY("room_mtime"), extract_long(&buf[4], 1))); + + StrBuf *sj = NewStrBuf(); + SerializeJson(sj, j, 1); // '1' == free the source array + add_response_header(h, strdup("Content-type"), strdup("application/json")); + h->response_code = 200; + h->response_string = strdup("OK"); + h->response_body_length = StrLength(sj); + h->response_body = SmashStrBuf(&sj); + } + else { + do_404(h); + } + return; +} + + +// Client is requesting a mailbox summary of the current room +void json_mailbox(struct http_transaction *h, struct ctdlsession *c) { + char buf[1024]; + char field[1024]; + JsonValue *j = NewJsonArray(HKEY("msgs")); + + ctdl_printf(c, "MSGS ALL|||9"); // "9" as the fourth parameter delivers a mailbox summary + ctdl_readline(c, buf, sizeof(buf)); + if (buf[0] == '1') { + while (ctdl_readline(c, buf, sizeof(buf)), (strcmp(buf, "000"))) { + utf8ify_rfc822_string(buf); + JsonValue *jmsg = NewJsonObject(HKEY("message")); + JsonObjectAppend(jmsg, NewJsonNumber(HKEY("msgnum"), extract_long(buf, 0))); + JsonObjectAppend(jmsg, NewJsonNumber(HKEY("time"), extract_long(buf, 1))); + extract_token(field, buf, 2, '|', sizeof field); + JsonObjectAppend(jmsg, NewJsonPlainString(HKEY("author"), field, -1)); + extract_token(field, buf, 4, '|', sizeof field); + JsonObjectAppend(jmsg, NewJsonPlainString(HKEY("addr"), field, -1)); + extract_token(field, buf, 5, '|', sizeof field); + JsonObjectAppend(jmsg, NewJsonPlainString(HKEY("subject"), field, -1)); + JsonObjectAppend(jmsg, NewJsonNumber(HKEY("msgidhash"), extract_long(buf, 6))); + extract_token(field, buf, 7, '|', sizeof field); + JsonObjectAppend(jmsg, NewJsonPlainString(HKEY("references"), field, -1)); + JsonArrayAppend(j, jmsg); // add the message to the array + } + } + + StrBuf *sj = NewStrBuf(); + SerializeJson(sj, j, 1); // '1' == free the source array + + add_response_header(h, strdup("Content-type"), strdup("application/json")); + h->response_code = 200; + h->response_string = strdup("OK"); + h->response_body_length = StrLength(sj); + h->response_body = SmashStrBuf(&sj); + return; +} + + +// Client is requesting a message list +void json_msglist(struct http_transaction *h, struct ctdlsession *c, char *which) { + int i = 0; + long *msglist = get_msglist(c, which); + JsonValue *j = NewJsonArray(HKEY("msgs")); + + if (msglist != NULL) { + for (i = 0; msglist[i] > 0; ++i) { + JsonArrayAppend(j, NewJsonNumber(HKEY("m"), msglist[i])); + } + free(msglist); + } + + StrBuf *sj = NewStrBuf(); + SerializeJson(sj, j, 1); // '1' == free the source array + + add_response_header(h, strdup("Content-type"), strdup("application/json")); + h->response_code = 200; + h->response_string = strdup("OK"); + h->response_body_length = StrLength(sj); + h->response_body = SmashStrBuf(&sj); + return; +} + + +// Client is requesting the room info banner +void read_room_info_banner(struct http_transaction *h, struct ctdlsession *c) { + char buf[1024]; + + ctdl_printf(c, "RINF"); + ctdl_readline(c, buf, sizeof(buf)); + if (buf[0] == '1') { + StrBuf *info = NewStrBuf(); + while (ctdl_readline(c, buf, sizeof(buf)), (strcmp(buf, "000"))){ + StrBufAppendPrintf(info, "%s\n", buf); + } + add_response_header(h, strdup("Content-type"), strdup("text/plain")); + h->response_code = 200; + h->response_string = strdup("OK"); + h->response_body_length = StrLength(info); + h->response_body = SmashStrBuf(&info); + return; + } + else { + do_404(h); + } +} + + +// Client is setting the "Last Read Pointer" (marking all messages as "seen" up to this message) +void set_last_read_pointer(struct http_transaction *h, struct ctdlsession *c) { + char cbuf[1024]; + ctdl_printf(c, "SLRP %d", atoi(get_url_param(h, "last"))); + ctdl_readline(c, cbuf, sizeof(cbuf)); + if (cbuf[0] == '2') { + do_204(h); + } + else { + do_404(h); + } +} + + +// Client requested an object in a room. +void object_in_room(struct http_transaction *h, struct ctdlsession *c) { + char buf[1024]; + long msgnum = (-1); + char unescaped_euid[1024]; + + extract_token(buf, h->url, 4, '/', sizeof buf); + + if (!strcasecmp(buf, "mailbox")) { // Client is requesting a mailbox summary + json_mailbox(h, c); + return; + } + + if (!strcasecmp(buf, "stat")) { // Client is requesting a stat command (name and modification time) + json_stat(h, c); + return; + } + + if (!strncasecmp(buf, "msgs.", 5)) { // Client is requesting a list of message numbers + unescape_input(&buf[5]); + json_msglist(h, c, &buf[5]); + return; + } + + if (!strcasecmp(buf, "info.txt")) { // Client is requesting the room info banner + read_room_info_banner(h, c); + return; + } + + if (!strcasecmp(buf, "slrp")) { // Set the Last Read Pointer + set_last_read_pointer(h, c); + return; + } + + // If we get to this point, the client is requesting a specific object *in* the room. + + if ((c->room_default_view == VIEW_CALENDAR) // room types where objects are referenced by EUID + || (c->room_default_view == VIEW_TASKS) + || (c->room_default_view == VIEW_ADDRESSBOOK) + ) { + safestrncpy(unescaped_euid, buf, sizeof unescaped_euid); + unescape_input(unescaped_euid); + msgnum = locate_message_by_uid(c, unescaped_euid); + } + else { // otherwise the object is being referenced by message number + msgnum = atol(buf); + } + + // All methods except PUT require the message to already exist + if ((msgnum <= 0) && (strcasecmp(h->method, "PUT"))) { + do_404(h); + } + + // If we get to this point we have a valid message number in an accessible room. + syslog(LOG_DEBUG, "msgnum is %ld, method is %s", msgnum, h->method); + + // A sixth component in the URL can be one of two things: + // (1) a MIME part specifier, in which case the client wants to download that component within the message + // (2) a content-type, in which ase the client wants us to try to render it a certain way + if (num_tokens(h->url, '/') == 6) { + extract_token(buf, h->url, 5, '/', sizeof buf); + if (!IsEmptyStr(buf)) { + if (!strcasecmp(buf, "json")) { + json_render_one_message(h, c, msgnum); + } + else { + download_mime_component(h, c, msgnum, buf); + } + return; + } + } + + // Ok, we want a full message, but first let's check for the if[-none]-match headers. + char *if_match = header_val(h, "If-Match"); + if ((if_match != NULL) && (!match_etags(if_match, msgnum))) { + do_412(h); + return; + } + + char *if_none_match = header_val(h, "If-None-Match"); + if ((if_none_match != NULL) && (match_etags(if_none_match, msgnum))) { + do_412(h); + return; + } + + // DOOOOOO ITTTTT!!! + + if (!strcasecmp(h->method, "DELETE")) { + dav_delete_message(h, c, msgnum); + } + else if (!strcasecmp(h->method, "GET")) { + dav_get_message(h, c, msgnum); + } + else if (!strcasecmp(h->method, "PUT")) { + dav_put_message(h, c, unescaped_euid, msgnum); + } + else { + do_404(h); // Got this far but the method made no sense? Bummer. + } + +} + + +// Called by the_room_itself() when the HTTP method is REPORT +void report_the_room_itself(struct http_transaction *h, struct ctdlsession *c) { + if (c->room_default_view == VIEW_CALENDAR) { + caldav_report(h, c); // CalDAV REPORTs ... fmgwac + return; + } + + do_404(h); // future implementations like CardDAV will require code paths here +} + + +// Called by the_room_itself() when the HTTP method is OPTIONS +void options_the_room_itself(struct http_transaction *h, struct ctdlsession *c) { + h->response_code = 200; + h->response_string = strdup("OK"); + if (c->room_default_view == VIEW_CALENDAR) { + add_response_header(h, strdup("DAV"), strdup("1, calendar-access")); // offer CalDAV + } + else if (c->room_default_view == VIEW_ADDRESSBOOK) { + add_response_header(h, strdup("DAV"), strdup("1, addressbook")); // offer CardDAV + } + else { + add_response_header(h, strdup("DAV"), strdup("1")); // ordinary WebDAV for all other room types + } + add_response_header(h, strdup("Allow"), strdup("OPTIONS, PROPFIND, GET, PUT, REPORT, DELETE")); +} + + +// Called by the_room_itself() when the HTTP method is PROPFIND +void propfind_the_room_itself(struct http_transaction *h, struct ctdlsession *c) { + char *e; + long timestamp; + int dav_depth = (header_val(h, "Depth") ? atoi(header_val(h, "Depth")) : INT_MAX); + syslog(LOG_DEBUG, "Client PROPFIND requested depth: %d", dav_depth); + StrBuf *Buf = NewStrBuf(); + + StrBufAppendPrintf(Buf, "" + ""); + + // Transmit the collection resource + StrBufAppendPrintf(Buf, ""); + StrBufAppendPrintf(Buf, ""); + StrBufXMLEscAppend(Buf, NULL, h->site_prefix, strlen(h->site_prefix), 0); + StrBufAppendPrintf(Buf, "/ctdl/r/"); + StrBufXMLEscAppend(Buf, NULL, c->room, strlen(c->room), 0); + StrBufAppendPrintf(Buf, ""); + + StrBufAppendPrintf(Buf, ""); + StrBufAppendPrintf(Buf, "HTTP/1.1 200 OK"); + StrBufAppendPrintf(Buf, ""); + StrBufAppendPrintf(Buf, ""); + StrBufXMLEscAppend(Buf, NULL, c->room, strlen(c->room), 0); + StrBufAppendPrintf(Buf, ""); + + StrBufAppendPrintf(Buf, ""); // empty owner ought to be legal; see rfc3744 section 5.1 + + StrBufAppendPrintf(Buf, ""); + switch (c->room_default_view) { + case VIEW_CALENDAR: + StrBufAppendPrintf(Buf, ""); // RFC4791 section 4.2 + break; + } + StrBufAppendPrintf(Buf, ""); + + int enumerate_by_euid = 0; // nonzero if messages will be retrieved by euid instead of msgnum + switch (c->room_default_view) { + case VIEW_CALENDAR: // RFC4791 section 5.2 + StrBufAppendPrintf(Buf, ""); + StrBufAppendPrintf(Buf, ""); + StrBufAppendPrintf(Buf, ""); + StrBufAppendPrintf(Buf, ""); + enumerate_by_euid = 1; + break; + case VIEW_TASKS: // RFC4791 section 5.2 + StrBufAppendPrintf(Buf, ""); + StrBufAppendPrintf(Buf, ""); + StrBufAppendPrintf(Buf, ""); + StrBufAppendPrintf(Buf, ""); + enumerate_by_euid = 1; + break; + case VIEW_ADDRESSBOOK: // FIXME put some sort of CardDAV crapola here when we implement it + enumerate_by_euid = 1; + break; + case VIEW_WIKI: // FIXME invent "WikiDAV" ? The versioning stuff in DAV could be useful. + enumerate_by_euid = 1; + break; + } + + // FIXME get the mtime + // StrBufAppendPrintf(Buf, ""); + // escputs(datestring); + // StrBufAppendPrintf(Buf, ""); + + StrBufAppendPrintf(Buf, ""); + StrBufAppendPrintf(Buf, ""); + StrBufAppendPrintf(Buf, "\n"); + + // If a depth greater than zero was specified, transmit the collection listing + // BEGIN COLLECTION + if (dav_depth > 0) { + long *msglist = get_msglist(c, "ALL"); + if (msglist) { + int i; + for (i = 0; (msglist[i] > 0); ++i) { + if ((i % 10) == 0) + syslog(LOG_DEBUG, "PROPFIND enumerated %d messages", i); + e = NULL; // EUID gets stored here + timestamp = 0; + + char cbuf[1024]; + ctdl_printf(c, "MSG0 %ld|3", msglist[i]); + ctdl_readline(c, cbuf, sizeof(cbuf)); + if (cbuf[0] == '1') + while (ctdl_readline(c, cbuf, sizeof(cbuf)), strcmp(cbuf, "000")) { + if ((enumerate_by_euid) && (!strncasecmp(cbuf, "exti=", 5))) { + // e = strdup(&cbuf[5]); + int elen = (2 * strlen(&cbuf[5])); + e = malloc(elen); + urlesc(e, elen, &cbuf[5]); + } + if (!strncasecmp(cbuf, "time=", 5)) { + timestamp = atol(&cbuf[5]); + } + } + if (e == NULL) { + e = malloc(20); + sprintf(e, "%ld", msglist[i]); + } + StrBufAppendPrintf(Buf, ""); + + // Generate the 'href' tag for this message + StrBufAppendPrintf(Buf, ""); + StrBufXMLEscAppend(Buf, NULL, h->site_prefix, strlen(h->site_prefix), 0); + StrBufAppendPrintf(Buf, "/ctdl/r/"); + StrBufXMLEscAppend(Buf, NULL, c->room, strlen(c->room), 0); + StrBufAppendPrintf(Buf, "/"); + StrBufXMLEscAppend(Buf, NULL, e, strlen(e), 0); + StrBufAppendPrintf(Buf, ""); + StrBufAppendPrintf(Buf, ""); + StrBufAppendPrintf(Buf, "HTTP/1.1 200 OK"); + StrBufAppendPrintf(Buf, ""); + + switch (c->room_default_view) { + case VIEW_CALENDAR: + StrBufAppendPrintf(Buf, + "text/calendar; component=vevent"); + break; + case VIEW_TASKS: + StrBufAppendPrintf(Buf, + "text/calendar; component=vtodo"); + break; + case VIEW_ADDRESSBOOK: + StrBufAppendPrintf(Buf, "text/x-vcard"); + break; + } + + if (timestamp > 0) { + char *datestring = http_datestring(timestamp); + if (datestring) { + StrBufAppendPrintf(Buf, ""); + StrBufXMLEscAppend(Buf, NULL, datestring, strlen(datestring), 0); + StrBufAppendPrintf(Buf, ""); + free(datestring); + } + if (enumerate_by_euid) { // FIXME ajc 2017oct30 should this be inside the timestamp conditional? + StrBufAppendPrintf(Buf, "\"%ld\"", msglist[i]); + } + } + StrBufAppendPrintf(Buf, "\n"); + free(e); + } + free(msglist); + }; + } + // END COLLECTION + + StrBufAppendPrintf(Buf, "\n"); + + add_response_header(h, strdup("Content-type"), strdup("text/xml")); + h->response_code = 207; + h->response_string = strdup("Multi-Status"); + h->response_body_length = StrLength(Buf); + h->response_body = SmashStrBuf(&Buf); +} + +// some good examples here +// http://blogs.nologin.es/rickyepoderi/index.php?/archives/14-Introducing-CalDAV-Part-I.html + + +// Called by the_room_itself() when the HTTP method is PROPFIND +void get_the_room_itself(struct http_transaction *h, struct ctdlsession *c) { + JsonValue *j = NewJsonObject(HKEY("gotoroom")); + + JsonObjectAppend(j, NewJsonPlainString(HKEY("name"), c->room, -1)); + JsonObjectAppend(j, NewJsonNumber(HKEY("current_view"), c->room_current_view)); + JsonObjectAppend(j, NewJsonNumber(HKEY("default_view"), c->room_default_view)); + JsonObjectAppend(j, NewJsonNumber(HKEY("is_room_aide"), c->is_room_aide)); + JsonObjectAppend(j, NewJsonNumber(HKEY("can_delete_messages"), c->can_delete_messages)); + JsonObjectAppend(j, NewJsonNumber(HKEY("new_messages"), c->new_messages)); + JsonObjectAppend(j, NewJsonNumber(HKEY("total_messages"), c->total_messages)); + JsonObjectAppend(j, NewJsonNumber(HKEY("last_seen"), c->last_seen)); + JsonObjectAppend(j, NewJsonNumber(HKEY("room_mtime"), c->room_mtime)); + JsonObjectAppend(j, NewJsonNumber(HKEY("new_mail"), c->new_mail)); + + StrBuf *sj = NewStrBuf(); + SerializeJson(sj, j, 1); // '1' == free the source array + + add_response_header(h, strdup("Content-type"), strdup("application/json")); + h->response_code = 200; + h->response_string = strdup("OK"); + h->response_body_length = StrLength(sj); + h->response_body = SmashStrBuf(&sj); + return; +} + + +// Handle REST/DAV requests for the room itself (such as /ctdl/r/roomname +// or /ctdl/r/roomname/ but *not* specific objects within the room) +void the_room_itself(struct http_transaction *h, struct ctdlsession *c) { + + // OPTIONS method on the room itself usually is a DAV client assessing what's here. + if (!strcasecmp(h->method, "OPTIONS")) { + options_the_room_itself(h, c); + return; + } + + // PROPFIND method on the room itself could be looking for a directory + if (!strcasecmp(h->method, "PROPFIND")) { + propfind_the_room_itself(h, c); + return; + } + + // REPORT method on the room itself is probably the dreaded CalDAV tower-of-crapola + if (!strcasecmp(h->method, "REPORT")) { + report_the_room_itself(h, c); + return; + } + + // GET method on the room itself is an API call, possibly from our JavaScript front end + if (!strcasecmp(h->method, "get")) { + get_the_room_itself(h, c); + return; + } + + // we probably want a "go to this room" for interactive access + do_404(h); +} + + +// Dispatcher for "/ctdl/r" and "/ctdl/r/" for the room list +void room_list(struct http_transaction *h, struct ctdlsession *c) { + char buf[1024]; + char roomname[1024]; + + ctdl_printf(c, "LKRA"); + ctdl_readline(c, buf, sizeof(buf)); + if (buf[0] != '1') { + do_502(h); + return; + } + + JsonValue *j = NewJsonArray(HKEY("lkra")); + while (ctdl_readline(c, buf, sizeof(buf)), strcmp(buf, "000")) { + + // 0 |1 |2 |3 |4 |5 |6 |7 |8 + // name|QRflags|QRfloor|QRorder|QRflags2|ra|current_view|default_view|mtime + JsonValue *jr = NewJsonObject(HKEY("room")); + + extract_token(roomname, buf, 0, '|', sizeof roomname); + JsonObjectAppend(jr, NewJsonPlainString(HKEY("name"), roomname, -1)); + + JsonObjectAppend(jr, NewJsonNumber(HKEY("floor"), extract_int(buf, 2))); + JsonObjectAppend(jr, NewJsonNumber(HKEY("rorder"), extract_int(buf, 3))); + + int ra = extract_int(buf, 5); + JsonObjectAppend(jr, NewJsonBool(HKEY("known"), (ra & UA_KNOWN))); + JsonObjectAppend(jr, NewJsonBool(HKEY("hasnewmsgs"), (ra & UA_HASNEWMSGS))); + + JsonObjectAppend(jr, NewJsonNumber(HKEY("current_view"), extract_int(buf, 6))); + JsonObjectAppend(jr, NewJsonNumber(HKEY("default_view"), extract_int(buf, 7))); + JsonObjectAppend(jr, NewJsonNumber(HKEY("mtime"), extract_int(buf, 8))); + + JsonArrayAppend(j, jr); // add the room to the array + } + + StrBuf *sj = NewStrBuf(); + SerializeJson(sj, j, 1); // '1' == free the source array + + add_response_header(h, strdup("Content-type"), strdup("application/json")); + h->response_code = 200; + h->response_string = strdup("OK"); + h->response_body_length = StrLength(sj); + h->response_body = SmashStrBuf(&sj); +} + + +// Dispatcher for paths starting with /ctdl/r/ +void ctdl_r(struct http_transaction *h, struct ctdlsession *c) { + char requested_roomname[128]; + char buf[1024]; + long room_flags = 0; + long room_flags2 = 0; + + // All room-related functions require being "in" the room specified. Are we in that room already? + extract_token(requested_roomname, h->url, 3, '/', sizeof requested_roomname); + unescape_input(requested_roomname); + + if (IsEmptyStr(requested_roomname)) { // /ctdl/r/ + room_list(h, c); + return; + } + // If not, try to go there. + if (strcasecmp(requested_roomname, c->room)) { + ctdl_printf(c, "GOTO %s", requested_roomname); + ctdl_readline(c, buf, sizeof(buf)); + if (buf[0] == '2') { + // buf[3] will indicate whether any instant messages are waiting + extract_token(c->room, &buf[4], 0, '|', sizeof c->room); + c->new_messages = extract_int(&buf[4], 1); + c->total_messages = extract_int(&buf[4], 2); + // 3 (int)info Info flag: set to nonzero if the user needs to read this room's info file + room_flags = extract_long(&buf[4], 3); // Various flags associated with this room. + // 5 (long)CC->room.QRhighest The highest message number present in this room + c->last_seen = extract_long(&buf[4], 6); // The highest message number the user has read in this room + // 7 (int)rmailflag Boolean flag: 1 if this is a Mail> room, 0 otherwise. + c->is_room_aide = extract_int(&buf[4], 8); + c->new_mail = extract_int(&buf[4], 9); // the number of new messages in the user's INBOX + // 10 (int)CC->room.QRfloor The floor number this room resides on + c->room_current_view = extract_int(&buf[4], 11); + c->room_default_view = extract_int(&buf[4], 12); + // 13 (int)is_trash Boolean flag: 1 if this is the user's Trash folder, 0 otherwise. + room_flags2 = extract_long(&buf[4], 14); // More flags associated with this room. + c->room_mtime = extract_long(&buf[4], 15); // Timestamp of the last write activity in this room + + // If any of these three conditions are met, let the client know it has permission to delete messages. + if ((c->is_room_aide) || (room_flags & QR_MAILBOX) || (room_flags2 & QR2_COLLABDEL)) { + c->can_delete_messages = 1; + } + else { + c->can_delete_messages = 0; + } + } + else { + do_404(h); + return; + } + } + // At this point our Citadel client session is "in" the specified room. + + if (num_tokens(h->url, '/') == 4) { // /ctdl/r/roomname + the_room_itself(h, c); + return; + } + + extract_token(buf, h->url, 4, '/', sizeof buf); + if (num_tokens(h->url, '/') == 5) { + if (IsEmptyStr(buf)) { + the_room_itself(h, c); // /ctdl/r/roomname/ ( same as /ctdl/r/roomname ) + } + else { + object_in_room(h, c); // /ctdl/r/roomname/object + } + return; + } + if (num_tokens(h->url, '/') == 6) { + object_in_room(h, c); // /ctdl/r/roomname/object/ or possibly /ctdl/r/roomname/object/component + return; + } + // If we get to this point, the client specified a valid room but requested an action we don't know how to perform. + do_404(h); +} diff --git a/webcit-ng/server/static.c b/webcit-ng/server/static.c new file mode 100644 index 000000000..632773da6 --- /dev/null +++ b/webcit-ng/server/static.c @@ -0,0 +1,63 @@ +// Output static content +// +// Copyright (c) 1996-2022 by the citadel.org team +// +// This program is open source software. Use, duplication, or +// disclosure are subject to the GNU General Public License v3. + +#include "webcit.h" + + +// Called from perform_request() to handle static content. +void output_static(struct http_transaction *h) { + char filename[PATH_MAX]; + struct stat statbuf; + + syslog(LOG_DEBUG, "static: %s", h->url); + + if (!strncasecmp(h->url, "/ctdl/s/", 8)) { + snprintf(filename, sizeof filename, "static/%s", &h->url[8]); + } + else if (!strncasecmp(h->url, "/.well-known/", 13)) { + snprintf(filename, sizeof filename, "static/.well-known/%s", &h->url[13]); + } + else if (!strcasecmp(h->url, "/favicon.ico")) { + snprintf(filename, sizeof filename, "static/images/favicon.ico"); + } + else { + do_404(h); + return; + } + + if (strstr(filename, "../")) { // 100% guaranteed attacker. + do_404(h); // Die in a car fire. + return; + } + + FILE *fp = fopen(filename, "r"); // Try to open the requested file. + if (fp == NULL) { + syslog(LOG_DEBUG, "%s: %s", filename, strerror(errno)); + do_404(h); + return; + } + + fstat(fileno(fp), &statbuf); + h->response_body_length = statbuf.st_size; + h->response_body = malloc(h->response_body_length); + if (h->response_body != NULL) { + fread(h->response_body, h->response_body_length, 1, fp); + } + else { + h->response_body_length = 0; + } + fclose(fp); // Content is now in memory. + + h->response_code = 200; + h->response_string = strdup("OK"); + add_response_header(h, strdup("Content-type"), strdup(GuessMimeByFilename(filename, strlen(filename)))); + + char *datestring = http_datestring(statbuf.st_mtime); + if (datestring) { + add_response_header(h, strdup("Last-Modified"), datestring); + } +} diff --git a/webcit-ng/server/tcp_sockets.c b/webcit-ng/server/tcp_sockets.c new file mode 100644 index 000000000..5de373ac9 --- /dev/null +++ b/webcit-ng/server/tcp_sockets.c @@ -0,0 +1,185 @@ +// +// TCP sockets layer +// +// Copyright (c) 1987-2022 by the citadel.org team +// +// This program is open source software. Use, duplication, or +// disclosure are subject to the GNU General Public License v3. + +#include "webcit.h" + +// lingering_close() a`la Apache. see +// http://httpd.apache.org/docs/2.0/misc/fin_wait_2.html for rationale +int lingering_close(int fd) { + char buf[SIZ]; + int i; + fd_set set; + struct timeval tv, start; + + gettimeofday(&start, NULL); + if (fd == -1) + return -1; + shutdown(fd, 1); + do { + do { + gettimeofday(&tv, NULL); + tv.tv_sec = SLEEPING - (tv.tv_sec - start.tv_sec); + tv.tv_usec = start.tv_usec - tv.tv_usec; + if (tv.tv_usec < 0) { + tv.tv_sec--; + tv.tv_usec += 1000000; + } + FD_ZERO(&set); + FD_SET(fd, &set); + i = select(fd + 1, &set, NULL, NULL, &tv); + } while (i == -1 && errno == EINTR); + + if (i <= 0) + break; + + i = read(fd, buf, sizeof buf); + } while (i != 0 && (i != -1 || errno == EINTR)); + + return close(fd); +} + + +// This is a generic function to set up a master socket for listening on +// a TCP port. The server shuts down if the bind fails. (IPv4/IPv6 version) +// +// ip_addr IP address to bind +// port_number port number to bind +// queue_len number of incoming connections to allow in the queue +int webcit_tcp_server(const char *ip_addr, int port_number, int queue_len) { + const char *ipv4broadcast = "0.0.0.0"; + int IsDefault = 0; + struct protoent *p; + struct sockaddr_in6 sin6; + struct sockaddr_in sin4; + int s, i, b; + int ip_version = 6; + +retry: + memset(&sin6, 0, sizeof(sin6)); + memset(&sin4, 0, sizeof(sin4)); + sin6.sin6_family = AF_INET6; + sin4.sin_family = AF_INET; + + if ( (ip_addr == NULL) // any IPv6 + || (IsEmptyStr(ip_addr)) + || (!strcmp(ip_addr, "*")) + ) { + IsDefault = 1; + ip_version = 6; + sin6.sin6_addr = in6addr_any; + } + else if (!strcmp(ip_addr, "0.0.0.0")) { // any IPv4 + ip_version = 4; + sin4.sin_addr.s_addr = INADDR_ANY; + } + else if ((strchr(ip_addr, '.')) && (!strchr(ip_addr, ':'))) { // specific IPv4 + ip_version = 4; + if (inet_pton(AF_INET, ip_addr, &sin4.sin_addr) <= 0) { + syslog(LOG_WARNING, "Error binding to [%s] : %s\n", ip_addr, strerror(errno)); + return (-1); + } + } + else { // specific IPv6 + + ip_version = 6; + if (inet_pton(AF_INET6, ip_addr, &sin6.sin6_addr) <= 0) { + syslog(LOG_WARNING, "Error binding to [%s] : %s\n", ip_addr, strerror(errno)); + return (-1); + } + } + + if (port_number == 0) { + syslog(LOG_WARNING, "Cannot start: no port number specified.\n"); + return (-1); + } + sin6.sin6_port = htons((u_short) port_number); + sin4.sin_port = htons((u_short) port_number); + + p = getprotobyname("tcp"); + + s = socket(((ip_version == 6) ? PF_INET6 : PF_INET), SOCK_STREAM, (p->p_proto)); + if (s < 0) { + if (IsDefault && (errno == EAFNOSUPPORT)) { + s = 0; + ip_addr = ipv4broadcast; + goto retry; + } + syslog(LOG_WARNING, "Can't create a listening socket: %s\n", strerror(errno)); + return (-1); + } + + // Set some socket options that make sense. + i = 1; + setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(i)); + + if (ip_version == 6) { + b = bind(s, (struct sockaddr *) &sin6, sizeof(sin6)); + } + else { + b = bind(s, (struct sockaddr *) &sin4, sizeof(sin4)); + } + + if (b < 0) { + syslog(LOG_ERR, "Can't bind: %s\n", strerror(errno)); + close(s); + return (-1); + } + + if (listen(s, queue_len) < 0) { + syslog(LOG_ERR, "Can't listen: %s\n", strerror(errno)); + close(s); + return (-1); + } + return (s); +} + + +// Create a Unix domain socket and listen on it +// sockpath - file name of the unix domain socket +// queue_len - Number of incoming connections to allow in the queue +int webcit_uds_server(char *sockpath, int queue_len) { + struct sockaddr_un addr; + int s; + int i; + int actual_queue_len; + + actual_queue_len = queue_len; + if (actual_queue_len < 5) + actual_queue_len = 5; + + i = unlink(sockpath); + if ((i != 0) && (errno != ENOENT)) { + syslog(LOG_WARNING, "webcit: can't unlink %s: %s", sockpath, strerror(errno)); + return (-1); + } + + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + safestrncpy(addr.sun_path, sockpath, sizeof addr.sun_path); + + s = socket(AF_UNIX, SOCK_STREAM, 0); + if (s < 0) { + syslog(LOG_WARNING, "webcit: Can't create a unix domain socket: %s", strerror(errno)); + return (-1); + } + + if (bind(s, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + syslog(LOG_WARNING, "webcit: Can't bind: %s", strerror(errno)); + close(s); + return (-1); + } + + if (listen(s, actual_queue_len) < 0) { + syslog(LOG_WARNING, "webcit: Can't listen: %s", strerror(errno)); + close(s); + return (-1); + } + + chmod(sockpath, 0777); + return (s); +} diff --git a/webcit-ng/server/text2html.c b/webcit-ng/server/text2html.c new file mode 100644 index 000000000..1a1bf1420 --- /dev/null +++ b/webcit-ng/server/text2html.c @@ -0,0 +1,104 @@ +// +// Convert text/plain to text/html +// +// Copyright (c) 2017-2022 by the citadel.org team +// +// This program is open source software. Use, duplication, or +// disclosure are subject to the GNU General Public License v3. + +#include "webcit.h" + + +// Convert a text/plain message to text/html +StrBuf *text2html(const char *supplied_charset, int treat_as_wiki, char *roomname, long msgnum, StrBuf *Source) { + StrBuf *sj = NULL; + + sj = NewStrBuf(); + if (!sj) { + return (sj); + } + + StrBufAppendPrintf(sj, "
");
+	StrEscAppend(sj, Source, NULL, 0, 0);	// FIXME - add code here to activate links
+	StrBufAppendPrintf(sj, "
\n"); + + return (sj); +} + + +// Convert a text/x-citadel-variformat message to text/html +StrBuf *variformat2html(StrBuf * Source) { + StrBuf *Target = NULL; + + Target = NewStrBuf(); + if (!Target) { + return (Target); + } + + const char *ptr, *pte; + const char *BufPtr = NULL; + StrBuf *Line = NewStrBufPlain(NULL, SIZ); + StrBuf *Line1 = NewStrBufPlain(NULL, SIZ); + StrBuf *Line2 = NewStrBufPlain(NULL, SIZ); + int bn = 0; + int bq = 0; + int i; + long len; + int intext = 0; + + if (StrLength(Source) > 0) + do { + StrBufSipLine(Line, Source, &BufPtr); + bq = 0; + i = 0; + ptr = ChrPtr(Line); + len = StrLength(Line); + pte = ptr + len; + + if ((intext == 1) && (isspace(*ptr))) { + StrBufAppendBufPlain(Target, HKEY("
"), 0); + } + intext = 1; + if (isspace(*ptr)) { + while ((ptr < pte) && ((*ptr == '>') || isspace(*ptr))) { + if (*ptr == '>') { + bq++; + } + ptr++; + i++; + } + } + + // Quoted text should be displayed in italics and in a + // different colour. This code understands Citadel-style + // " >" quotes and will convert to
tags. + if (i > 0) { + StrBufCutLeft(Line, i); + } + for (i = bn; i < bq; i++) { + StrBufAppendBufPlain(Target, HKEY("
"), 0); + } + for (i = bq; i < bn; i++) { + StrBufAppendBufPlain(Target, HKEY("
"), 0); + } + bn = bq; + + if (StrLength(Line) == 0) + continue; + + // Activate embedded URL's + UrlizeText(Line1, Line, Line2); + StrEscAppend(Target, Line1, NULL, 0, 0); + StrBufAppendBufPlain(Target, HKEY("\n"), 0); + } + while ((BufPtr != StrBufNOTNULL) && (BufPtr != NULL)); + + for (i = 0; i < bn; i++) { + StrBufAppendBufPlain(Target, HKEY("
"), 0); + } + StrBufAppendBufPlain(Target, HKEY("
\n"), 0); + FreeStrBuf(&Line); + FreeStrBuf(&Line1); + FreeStrBuf(&Line2); + return(Target); +} diff --git a/webcit-ng/server/tls.c b/webcit-ng/server/tls.c new file mode 100644 index 000000000..c0e26f0d4 --- /dev/null +++ b/webcit-ng/server/tls.c @@ -0,0 +1,207 @@ +// Functions in this module handle SSL encryption when WebCit is running +// as an HTTPS server. +// +// Copyright (c) 1996-2022 by the citadel.org team +// +// This program is open source software. Use, duplication, or +// disclosure are subject to the GNU General Public License v3. + +#include "webcit.h" + +SSL_CTX *ssl_ctx; // global SSL context +char key_file[PATH_MAX] = ""; +char cert_file[PATH_MAX] = ""; +char *ssl_cipher_list = DEFAULT_SSL_CIPHER_LIST; + + +// Set the private key and certificate chain for the global SSL Context. +// This is called during initialization, and can be called again later if the certificate changes. +void bind_to_key_and_certificate(void) { + SSL_CTX *old_ctx, *new_ctx; + + if (!(new_ctx = SSL_CTX_new(SSLv23_server_method()))) { + syslog(LOG_WARNING, "SSL_CTX_new failed: %s", ERR_reason_error_string(ERR_get_error())); + return; + } + + syslog(LOG_INFO, "Requesting cipher list: %s", ssl_cipher_list); + if (!(SSL_CTX_set_cipher_list(new_ctx, ssl_cipher_list))) { + syslog(LOG_WARNING, "SSL_CTX_set_cipher_list failed: %s", ERR_reason_error_string(ERR_get_error())); + return; + } + + if (IsEmptyStr(key_file)) { + snprintf(key_file, sizeof key_file, "%s/keys/citadel.key", ctdl_dir); + } + if (IsEmptyStr(cert_file)) { + snprintf(cert_file, sizeof key_file, "%s/keys/citadel.cer", ctdl_dir); + } + + syslog(LOG_DEBUG, "crypto: [re]installing key \"%s\" and certificate \"%s\"", key_file, cert_file); + + SSL_CTX_use_certificate_chain_file(new_ctx, cert_file); + SSL_CTX_use_PrivateKey_file(new_ctx, key_file, SSL_FILETYPE_PEM); + + if ( !SSL_CTX_check_private_key(new_ctx) ) { + syslog(LOG_WARNING, "crypto: cannot install certificate: %s", ERR_reason_error_string(ERR_get_error())); + } + + old_ctx = ssl_ctx; + ssl_ctx = new_ctx; + sleep(1); + SSL_CTX_free(old_ctx); +} + + +// Initialize ssl engine, load certs and initialize openssl internals +void init_ssl(void) { + + // Initialize the OpenSSL library + SSL_load_error_strings(); + ERR_load_crypto_strings(); + OpenSSL_add_all_algorithms(); + SSL_library_init(); + + // Now try to bind to the key and certificate. + bind_to_key_and_certificate(); +} + + +// Check the modification time of the key and certificate -- reload if they changed +void update_key_and_cert_if_needed(void) { + static time_t previous_mtime = 0; + struct stat keystat; + struct stat certstat; + + if (stat(key_file, &keystat) != 0) { + syslog(LOG_ERR, "%s: %s", key_file, strerror(errno)); + return; + } + if (stat(cert_file, &certstat) != 0) { + syslog(LOG_ERR, "%s: %s", cert_file, strerror(errno)); + return; + } + + if ((keystat.st_mtime + certstat.st_mtime) != previous_mtime) { + bind_to_key_and_certificate(); + previous_mtime = keystat.st_mtime + certstat.st_mtime; + } +} + + +// starts SSL/TLS encryption for the current session. +void starttls(struct client_handle *ch) { + int retval, bits, alg_bits; + + if (!ssl_ctx) { + return; + } + + // Check the modification time of the key and certificate -- reload if they changed + update_key_and_cert_if_needed(); + + if (!(ch->ssl_handle = SSL_new(ssl_ctx))) { + syslog(LOG_WARNING, "SSL_new failed: %s", ERR_reason_error_string(ERR_get_error())); + return; + } + if (!(SSL_set_fd(ch->ssl_handle, ch->sock))) { + syslog(LOG_WARNING, "SSL_set_fd failed: %s", ERR_reason_error_string(ERR_get_error())); + SSL_free(ch->ssl_handle); + return; + } + retval = SSL_accept(ch->ssl_handle); + if (retval < 1) { + syslog(LOG_WARNING, "SSL_accept failed: %s", ERR_reason_error_string(ERR_get_error())); + } + else { + syslog(LOG_INFO, "SSL_accept success"); + } + bits = SSL_CIPHER_get_bits(SSL_get_current_cipher(ch->ssl_handle), &alg_bits); + syslog(LOG_INFO, "SSL/TLS using %s on %s (%d of %d bits)", + SSL_CIPHER_get_name(SSL_get_current_cipher(ch->ssl_handle)), + SSL_CIPHER_get_version(SSL_get_current_cipher(ch->ssl_handle)), bits, alg_bits); + syslog(LOG_INFO, "SSL started"); +} + + +// shuts down the TLS connection +void endtls(struct client_handle *ch) { + syslog(LOG_INFO, "Ending SSL/TLS"); + if (ch->ssl_handle != NULL) { + SSL_shutdown(ch->ssl_handle); + SSL_get_SSL_CTX(ch->ssl_handle); + SSL_free(ch->ssl_handle); + } + ch->ssl_handle = NULL; +} + + +// Send binary data to the client encrypted. +int client_write_ssl(struct client_handle *ch, char *buf, int nbytes) { + int retval; + int nremain; + char junk[1]; + + if (ch->ssl_handle == NULL) + return (-1); + + nremain = nbytes; + while (nremain > 0) { + if (SSL_want_write(ch->ssl_handle)) { + if ((SSL_read(ch->ssl_handle, junk, 0)) < 1) { + syslog(LOG_WARNING, "SSL_read in client_write: %s", ERR_reason_error_string(ERR_get_error())); + } + } + retval = SSL_write(ch->ssl_handle, &buf[nbytes - nremain], nremain); + if (retval < 1) { + long errval; + + errval = SSL_get_error(ch->ssl_handle, retval); + if (errval == SSL_ERROR_WANT_READ || errval == SSL_ERROR_WANT_WRITE) { + sleep(1); + continue; + } + syslog(LOG_WARNING, "SSL_write: %s", ERR_reason_error_string(ERR_get_error())); + if (retval == -1) { + syslog(LOG_WARNING, "errno is %d", errno); + endtls(ch); + } + return -1; + } + nremain -= retval; + } + return 0; +} + + +// read data from the encrypted layer +int client_read_ssl(struct client_handle *ch, char *buf, int nbytes) { + int bytes_read = 0; + int rlen = 0; + char junk[1]; + + if (ch->ssl_handle == NULL) + return (-1); + + while (bytes_read < nbytes) { + if (SSL_want_read(ch->ssl_handle)) { + if ((SSL_write(ch->ssl_handle, junk, 0)) < 1) { + syslog(LOG_WARNING, "SSL_write in client_read"); + } + } + rlen = SSL_read(ch->ssl_handle, &buf[bytes_read], nbytes - bytes_read); + if (rlen < 1) { + long errval; + errval = SSL_get_error(ch->ssl_handle, rlen); + if (errval == SSL_ERROR_WANT_READ || errval == SSL_ERROR_WANT_WRITE) { + sleep(1); + continue; + } + syslog(LOG_WARNING, "SSL_read error %ld", errval); + endtls(ch); + return (-1); + } + bytes_read += rlen; + } + return (bytes_read); +} diff --git a/webcit-ng/server/user_functions.c b/webcit-ng/server/user_functions.c new file mode 100644 index 000000000..0b4548ec3 --- /dev/null +++ b/webcit-ng/server/user_functions.c @@ -0,0 +1,124 @@ +// User functions +// +// Copyright (c) 1996-2022 by the citadel.org team +// +// This program is open source software. Use, duplication, or +// disclosure are subject to the GNU General Public License v3. + +#include "webcit.h" + + +// Fetch a user photo (avatar) +void fetch_user_photo(struct http_transaction *h, struct ctdlsession *c, char *username) { + char buf[1024]; + int content_length = 0; + char content_type[1024]; + char *image = NULL; + int actual_length = 0; + + ctdl_printf(c, "DLUI %s", username); + ctdl_readline(c, buf, sizeof(buf)); + if (buf[0] == '6') { + content_length = extract_int(&buf[4], 0); + extract_token(content_type, &buf[4], 3, '|', sizeof content_type); + + image = malloc(content_length); + if (image == NULL) { + do_502(h); + return; + } + actual_length = ctdl_read_binary(c, image, content_length); + + add_response_header(h, strdup("Content-type"), strdup(content_type)); + h->response_code = 200; + h->response_string = strdup("OK"); + h->response_body_length = actual_length; + h->response_body = image; + return; + } + + do_404(h); +} + + +// Fetch a user bio (profile) +void fetch_user_bio(struct http_transaction *h, struct ctdlsession *c, char *username) { + do_404(h); // FIXME finish this +} + + +// Client requested an object related to a user. +void object_in_user(struct http_transaction *h, struct ctdlsession *c, char *requested_username) { + char object_name[1024]; + + extract_token(object_name, h->url, 4, '/', sizeof object_name); + + if (!strcasecmp(object_name, "userpic")) { // user photo (avatar) + fetch_user_photo(h, c, requested_username); + return; + } + + if (!strcasecmp(object_name, "bio")) { // user bio (profile) + fetch_user_bio(h, c, requested_username); + return; + } + + do_404(h); // unknown object + return; +} + + +// Handle REST/DAV requests for the user itself (such as /ctdl/u/username +// or /ctdl/i/username/ but *not* specific properties of the user) +void the_user_itself(struct http_transaction *h, struct ctdlsession *c, char *username) { + do_404(h); +} + + +// Dispatcher for "/ctdl/u" and "/ctdl/u/" for the user list +void user_list(struct http_transaction *h, struct ctdlsession *c) { + do_404(h); +} + + +// Dispatcher for paths starting with /ctdl/u/ +void ctdl_u(struct http_transaction *h, struct ctdlsession *c) { + char requested_username[128]; + char buf[1024]; + + // All user-related functions require accessing the user in question. + extract_token(requested_username, h->url, 3, '/', sizeof requested_username); + unescape_input(requested_username); + + if (IsEmptyStr(requested_username)) { // /ctdl/u/ + user_list(h, c); + return; + } + + // At this point we have extracted the name of the user we're interested in. + // FIXME should we validate it? + + if (num_tokens(h->url, '/') == 4) { // /ctdl/u/username + the_user_itself(h, c, requested_username); + return; + } + + extract_token(buf, h->url, 4, '/', sizeof buf); + if (num_tokens(h->url, '/') == 5) { + if (IsEmptyStr(buf)) { + the_user_itself(h, c, requested_username); // /ctdl/u/username/ (same as /ctdl/u/username) + } + else { + object_in_user(h, c, requested_username); // /ctdl/u/username/object + } + return; + } + + if (num_tokens(h->url, '/') == 6) { + object_in_user(h, c, requested_username); // /ctdl/u/username/object/ or possibly /ctdl/u/username/object/component + return; + } + + // If we get to this point, the client requested an action we don't know how to perform. + do_404(h); +} diff --git a/webcit-ng/server/util.c b/webcit-ng/server/util.c new file mode 100644 index 000000000..72bf66038 --- /dev/null +++ b/webcit-ng/server/util.c @@ -0,0 +1,97 @@ +// +// Utility functions +// +// Copyright (c) 1996-2022 by the citadel.org team +// +// This program is open source software. Use, duplication, or +// disclosure are subject to the GNU General Public License v3. + +#include "webcit.h" + + +// remove escaped strings from i.e. the url string (like %20 for blanks) +int unescape_input(char *buf) { + unsigned int a, b; + char hex[3]; + long buflen; + long len; + + buflen = strlen(buf); + + while ((buflen > 0) && (isspace(buf[buflen - 1]))) { + buf[buflen - 1] = 0; + buflen--; + } + + a = 0; + while (a < buflen) { + if (buf[a] == '+') + buf[a] = ' '; + if (buf[a] == '%') { + // don't let % chars through, rather truncate the input. + if (a + 2 > buflen) { + buf[a] = '\0'; + buflen = a; + } + else { + hex[0] = buf[a + 1]; + hex[1] = buf[a + 2]; + hex[2] = 0; + b = 0; + b = decode_hex(hex); + buf[a] = (char) b; + len = buflen - a - 2; + if (len > 0) + memmove(&buf[a + 1], &buf[a + 3], len); + + buflen -= 2; + } + } + a++; + } + return a; +} + + +// Supplied with a unix timestamp, generate a textual time/date stamp. +// Caller owns the returned memory. +char *http_datestring(time_t xtime) { + + // HTTP Months - do not translate - these are not for human consumption + static char *httpdate_months[] = { + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" + }; + + // HTTP Weekdays - do not translate - these are not for human consumption + static char *httpdate_weekdays[] = { + "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" + }; + + struct tm t; + long offset; + char offsign; + int n = 40; + char *buf = malloc(n); + if (!buf) { + return (NULL); + } + + localtime_r(&xtime, &t); + + // Convert "seconds west of GMT" to "hours/minutes offset" + offset = t.tm_gmtoff; + if (offset > 0) { + offsign = '+'; + } + else { + offset = 0L - offset; + offsign = '-'; + } + offset = ((offset / 3600) * 100) + (offset % 60); + + snprintf(buf, n, "%s, %02d %s %04d %02d:%02d:%02d %c%04ld", + httpdate_weekdays[t.tm_wday], + t.tm_mday, httpdate_months[t.tm_mon], t.tm_year + 1900, t.tm_hour, t.tm_min, t.tm_sec, offsign, offset); + return (buf); +} diff --git a/webcit-ng/server/webcit.h b/webcit-ng/server/webcit.h new file mode 100644 index 000000000..9e6d069ba --- /dev/null +++ b/webcit-ng/server/webcit.h @@ -0,0 +1,165 @@ +// webcit.h - "header of headers" +// +// Copyright (c) 1996-2022 by the citadel.org team +// +// This program is open source software. You can redistribute it and/or +// modify it under the terms of the GNU General Public License, version 3. + +#define SHOW_ME_VAPPEND_PRINTF + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#define OPENSSL_NO_KRB5 // Work around RedHat's b0rken OpenSSL includes +#include +#include +#include +#include +#define _(x) x // temporary hack until we add i18n back in +//#define DEBUG_HTTP // uncomment to debug HTTP headers + +// XML_StopParser is present in expat 2.x +#if XML_MAJOR_VERSION > 1 +#define HAVE_XML_STOPPARSER +#endif + +struct client_handle { // this gets passed up the stack from the webserver to the application code + int sock; + SSL *ssl_handle; +}; + +struct keyval { // key/value pair (for array) + char *key; + char *val; +}; + +struct http_transaction { // The lifetime of an HTTP request goes through this data structure. + char *method; // The top half is built up by the web server and sent up to the + char *url; // application stack. The second half is built up by the application + char *http_version; // stack and sent back down to the web server, which transmits it to + char *site_prefix; // the client. + Array *request_headers; + char *request_body; + long request_body_length; + Array *request_parms; // anything after the "?" in the URL + int response_code; + char *response_string; + Array *response_headers; + char *response_body; + long response_body_length; +}; + +#define AUTH_MAX 256 // Maximum length of an HTTP AUTH header or equivalent cookie data +struct ctdlsession { + struct ctdlsession *next; + int is_bound; // Nonzero if this record is currently bound to a running thread + int sock; // Socket connection to Citadel server + char auth[AUTH_MAX]; // Auth string (empty if not logged in) + char whoami[64]; // Display name of currently logged in user (empty if not logged in) + char room[128]; // What room we are currently in + int room_current_view; + int room_default_view; + int is_room_aide; // nonzero if the user has aide rights to THIS room + int can_delete_messages; // nonzeri if the user is permitted to delete messages in THIS room + long last_seen; + int new_messages; + int total_messages; + time_t last_access; // Timestamp of last request that used this session + time_t num_requests_handled; + time_t room_mtime; // Timestampt of the most recent write activity in this room + int new_mail; // number of new messages in the user's INBOX +}; + +extern char *ssl_cipher_list; +extern int is_https; // nonzero if we are an HTTPS server today +extern char *ctdl_dir; // directory where Citadel Server is running +void init_ssl(void); +void starttls(struct client_handle *); +void endtls(struct client_handle *); +int client_write_ssl(struct client_handle *ch, char *buf, int nbytes); +int client_read_ssl(struct client_handle *ch, char *buf, int nbytes); + +enum { + WEBSERVER_HTTP, + WEBSERVER_HTTPS, + WEBSERVER_UDS +}; + +#define TRACE syslog(LOG_DEBUG, "\033[3%dmCHECKPOINT: %s:%d\033[0m", ((__LINE__%6)+1), __FILE__, __LINE__) +#define SLEEPING 180 // TCP connection timeout +#define MAX_WORKER_THREADS 32 // Maximum number of worker threads permitted to exist +#define DEFAULT_SSL_CIPHER_LIST "DEFAULT" // See http://openssl.org/docs/apps/ciphers.html +#define WEBSERVER_PORT 80 +#define WEBSERVER_INTERFACE "*" +#define CTDL_DIR "/usr/local/citadel" +#define DEVELOPER_ID 0 +#define CLIENT_ID 4 +#define TARGET "webcit02" /* Window target for inline URL's */ + +void worker_entry(int *); +void perform_one_http_transaction(struct client_handle *ch); +void add_response_header(struct http_transaction *h, char *key, char *val); +void perform_request(struct http_transaction *); +void do_204(struct http_transaction *); +void do_404(struct http_transaction *); +void do_412(struct http_transaction *); +void do_502(struct http_transaction *); +void output_static(struct http_transaction *); +int uds_connectsock(char *); +void ctdl_a(struct http_transaction *, struct ctdlsession *); +void ctdl_f(struct http_transaction *, struct ctdlsession *); +void ctdl_r(struct http_transaction *, struct ctdlsession *); +void ctdl_u(struct http_transaction *, struct ctdlsession *); +struct ctdlsession *connect_to_citadel(struct http_transaction *); +void disconnect_from_citadel(struct ctdlsession *); +char *header_val(struct http_transaction *h, char *requested_header); +char *get_url_param(struct http_transaction *h, char *requested_param); +int unescape_input(char *); +void http_redirect(struct http_transaction *h, char *to_where); +char *http_datestring(time_t xtime); +long *get_msglist(struct ctdlsession *c, char *which_msgs); +void caldav_report(struct http_transaction *h, struct ctdlsession *c); +long locate_message_by_uid(struct ctdlsession *c, char *uid); +void ctdl_delete_msgs(struct ctdlsession *c, long *msgnums, int num_msgs); +void dav_delete_message(struct http_transaction *h, struct ctdlsession *c, long msgnum); +void dav_get_message(struct http_transaction *h, struct ctdlsession *c, long msgnum); +void dav_put_message(struct http_transaction *h, struct ctdlsession *c, char *euid, long old_msgnum); +ssize_t ctdl_write(struct ctdlsession *ctdl, const void *buf, size_t count); +int login_to_citadel(struct ctdlsession *c, char *auth, char *resultbuf); +StrBuf *ctdl_readtextmsg(struct ctdlsession *ctdl); +StrBuf *html2html(const char *supplied_charset, int treat_as_wiki, char *roomname, long msgnum, StrBuf *Source); +void download_mime_component(struct http_transaction *h, struct ctdlsession *c, long msgnum, char *partnum); +StrBuf *text2html(const char *supplied_charset, int treat_as_wiki, char *roomname, long msgnum, StrBuf *Source); +StrBuf *variformat2html(StrBuf *Source); +int ctdl_readline(struct ctdlsession *ctdl, char *buf, int maxbytes); +int ctdl_read_binary(struct ctdlsession *ctdl, char *buf, int bytes_requested); +void ctdl_c(struct http_transaction *h, struct ctdlsession *c); +int webserver(char *webserver_interface, int webserver_port, int webserver_protocol); +void ctdl_printf(struct ctdlsession *ctdl, const char *format,...); +int webcit_tcp_server(const char *ip_addr, int port_number, int queue_len); +void UrlizeText(StrBuf* Target, StrBuf *Source, StrBuf *WrkBuf); +void json_render_one_message(struct http_transaction *h, struct ctdlsession *c, long msgnum); diff --git a/webcit-ng/server/webserver.c b/webcit-ng/server/webserver.c new file mode 100644 index 000000000..2e22efabd --- /dev/null +++ b/webcit-ng/server/webserver.c @@ -0,0 +1,147 @@ +// webserver.c +// +// This module handles the task of setting up a listening socket, accepting +// connections, and dispatching active connections onto a pool of worker +// threads. +// +// Copyright (c) 1996-2022 by the citadel.org team +// +// This program is open source software. Use, duplication, or +// disclosure are subject to the GNU General Public License v3. + +#include "webcit.h" // All other headers are included from this header. + +int num_threads_executing = 1; // Number of worker threads currently bound to a connected client +int num_threads_existing = 1; // Total number of worker threads that exist right now +int is_https = 0; // Set to nonzero if we are running as an HTTPS server today. +static void *original_brk = NULL; // Remember the original program break so we can test for leaks + + +// Spawn an additional worker thread into the pool. +void spawn_another_worker_thread(int *pointer_to_master_socket) { + pthread_t th; // Thread descriptor + pthread_attr_t attr; // Thread attributes + + // set attributes for the new thread + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + pthread_attr_setstacksize(&attr, 1048576); // Large stacks to prevent MIME parser crash on FreeBSD + + // now create the thread + if (pthread_create(&th, &attr, (void *(*)(void *)) worker_entry, (void *) pointer_to_master_socket) != 0) { + syslog(LOG_WARNING, "Can't create thread: %s", strerror(errno)); + } + else { + ++num_threads_existing; + ++num_threads_executing; + } + + // free up the attributes + pthread_attr_destroy(&attr); +} + + +// Entry point for worker threads +void worker_entry(int *pointer_to_master_socket) { + int master_socket = *pointer_to_master_socket; + int i = 0; + int fail_this_transaction = 0; + struct client_handle ch; + + while (1) { + // Each worker thread blocks on accept() while waiting for something to do. + // We don't have to worry about the "thundering herd" problem on modern kernels; for an explanation see + // https://stackoverflow.com/questions/2213779/does-the-thundering-herd-problem-exist-on-linux-anymore + memset(&ch, 0, sizeof ch); + ch.sock = -1; + errno = EAGAIN; + do { + --num_threads_executing; + syslog(LOG_DEBUG, "Additional memory allocated since startup: %d bytes", (int) (sbrk(0) - original_brk)); + syslog(LOG_DEBUG, "Thread 0x%x calling accept() on master socket %d", (unsigned int) pthread_self(), + master_socket); + ch.sock = accept(master_socket, NULL, 0); + if (ch.sock < 0) { + syslog(LOG_DEBUG, "accept() : %s", strerror(errno)); + } + ++num_threads_executing; + syslog(LOG_DEBUG, "socket %d is awake , threads executing: %d , threads total: %d", ch.sock, + num_threads_executing, num_threads_existing); + } while ((master_socket > 0) && (ch.sock < 0)); + + // If all threads are executing, spawn more, up to the maximum + if ((num_threads_executing >= num_threads_existing) && (num_threads_existing <= MAX_WORKER_THREADS)) { + spawn_another_worker_thread(pointer_to_master_socket); + } + + // We have a client. Do some work. + + // Set the SO_REUSEADDR socket option + i = 1; + setsockopt(ch.sock, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(i)); + + // If we are an HTTPS server, go crypto now. + if (is_https) { + starttls(&ch); + if (ch.ssl_handle == NULL) { + fail_this_transaction = 1; + } + } + else { + int fdflags; + fdflags = fcntl(ch.sock, F_GETFL); + if (fdflags < 0) { + syslog(LOG_WARNING, "unable to get server socket flags! %s", strerror(errno)); + } + } + + // Perform an HTTP transaction... + if (fail_this_transaction == 0) { + perform_one_http_transaction(&ch); + } + + // Shut down SSL/TLS if required... + if (is_https) { + endtls(&ch); + } + // ...and close the socket. + //syslog(LOG_DEBUG, "Closing socket %d...", ch.sock); + //lingering_close(ch.sock); + close(ch.sock); + syslog(LOG_DEBUG, "Closed socket %d.", ch.sock); + } +} + + +// Start up a TCP HTTP[S] server on the requested port +int webserver(char *webserver_interface, int webserver_port, int webserver_protocol) { + int master_socket = (-1); + original_brk = sbrk(0); + + switch (webserver_protocol) { + case WEBSERVER_HTTP: + syslog(LOG_DEBUG, "Starting HTTP server on %s:%d", webserver_interface, webserver_port); + master_socket = webcit_tcp_server(webserver_interface, webserver_port, 10); + break; + case WEBSERVER_HTTPS: + syslog(LOG_DEBUG, "Starting HTTPS server on %s:%d", webserver_interface, webserver_port); + master_socket = webcit_tcp_server(webserver_interface, webserver_port, 10); + init_ssl(); + is_https = 1; + break; + default: + syslog(LOG_ERR, "unknown protocol"); + ;; + } + + if (master_socket < 1) { + syslog(LOG_ERR, "Unable to bind the web server listening socket"); + return (1); + } + + syslog(LOG_INFO, "Listening on socket %d", master_socket); + signal(SIGPIPE, SIG_IGN); + + worker_entry(&master_socket); // this thread becomes a worker + return (0); +} diff --git a/webcit-ng/static.c b/webcit-ng/static.c deleted file mode 100644 index 632773da6..000000000 --- a/webcit-ng/static.c +++ /dev/null @@ -1,63 +0,0 @@ -// Output static content -// -// Copyright (c) 1996-2022 by the citadel.org team -// -// This program is open source software. Use, duplication, or -// disclosure are subject to the GNU General Public License v3. - -#include "webcit.h" - - -// Called from perform_request() to handle static content. -void output_static(struct http_transaction *h) { - char filename[PATH_MAX]; - struct stat statbuf; - - syslog(LOG_DEBUG, "static: %s", h->url); - - if (!strncasecmp(h->url, "/ctdl/s/", 8)) { - snprintf(filename, sizeof filename, "static/%s", &h->url[8]); - } - else if (!strncasecmp(h->url, "/.well-known/", 13)) { - snprintf(filename, sizeof filename, "static/.well-known/%s", &h->url[13]); - } - else if (!strcasecmp(h->url, "/favicon.ico")) { - snprintf(filename, sizeof filename, "static/images/favicon.ico"); - } - else { - do_404(h); - return; - } - - if (strstr(filename, "../")) { // 100% guaranteed attacker. - do_404(h); // Die in a car fire. - return; - } - - FILE *fp = fopen(filename, "r"); // Try to open the requested file. - if (fp == NULL) { - syslog(LOG_DEBUG, "%s: %s", filename, strerror(errno)); - do_404(h); - return; - } - - fstat(fileno(fp), &statbuf); - h->response_body_length = statbuf.st_size; - h->response_body = malloc(h->response_body_length); - if (h->response_body != NULL) { - fread(h->response_body, h->response_body_length, 1, fp); - } - else { - h->response_body_length = 0; - } - fclose(fp); // Content is now in memory. - - h->response_code = 200; - h->response_string = strdup("OK"); - add_response_header(h, strdup("Content-type"), strdup(GuessMimeByFilename(filename, strlen(filename)))); - - char *datestring = http_datestring(statbuf.st_mtime); - if (datestring) { - add_response_header(h, strdup("Last-Modified"), datestring); - } -} diff --git a/webcit-ng/tcp_sockets.c b/webcit-ng/tcp_sockets.c deleted file mode 100644 index 5de373ac9..000000000 --- a/webcit-ng/tcp_sockets.c +++ /dev/null @@ -1,185 +0,0 @@ -// -// TCP sockets layer -// -// Copyright (c) 1987-2022 by the citadel.org team -// -// This program is open source software. Use, duplication, or -// disclosure are subject to the GNU General Public License v3. - -#include "webcit.h" - -// lingering_close() a`la Apache. see -// http://httpd.apache.org/docs/2.0/misc/fin_wait_2.html for rationale -int lingering_close(int fd) { - char buf[SIZ]; - int i; - fd_set set; - struct timeval tv, start; - - gettimeofday(&start, NULL); - if (fd == -1) - return -1; - shutdown(fd, 1); - do { - do { - gettimeofday(&tv, NULL); - tv.tv_sec = SLEEPING - (tv.tv_sec - start.tv_sec); - tv.tv_usec = start.tv_usec - tv.tv_usec; - if (tv.tv_usec < 0) { - tv.tv_sec--; - tv.tv_usec += 1000000; - } - FD_ZERO(&set); - FD_SET(fd, &set); - i = select(fd + 1, &set, NULL, NULL, &tv); - } while (i == -1 && errno == EINTR); - - if (i <= 0) - break; - - i = read(fd, buf, sizeof buf); - } while (i != 0 && (i != -1 || errno == EINTR)); - - return close(fd); -} - - -// This is a generic function to set up a master socket for listening on -// a TCP port. The server shuts down if the bind fails. (IPv4/IPv6 version) -// -// ip_addr IP address to bind -// port_number port number to bind -// queue_len number of incoming connections to allow in the queue -int webcit_tcp_server(const char *ip_addr, int port_number, int queue_len) { - const char *ipv4broadcast = "0.0.0.0"; - int IsDefault = 0; - struct protoent *p; - struct sockaddr_in6 sin6; - struct sockaddr_in sin4; - int s, i, b; - int ip_version = 6; - -retry: - memset(&sin6, 0, sizeof(sin6)); - memset(&sin4, 0, sizeof(sin4)); - sin6.sin6_family = AF_INET6; - sin4.sin_family = AF_INET; - - if ( (ip_addr == NULL) // any IPv6 - || (IsEmptyStr(ip_addr)) - || (!strcmp(ip_addr, "*")) - ) { - IsDefault = 1; - ip_version = 6; - sin6.sin6_addr = in6addr_any; - } - else if (!strcmp(ip_addr, "0.0.0.0")) { // any IPv4 - ip_version = 4; - sin4.sin_addr.s_addr = INADDR_ANY; - } - else if ((strchr(ip_addr, '.')) && (!strchr(ip_addr, ':'))) { // specific IPv4 - ip_version = 4; - if (inet_pton(AF_INET, ip_addr, &sin4.sin_addr) <= 0) { - syslog(LOG_WARNING, "Error binding to [%s] : %s\n", ip_addr, strerror(errno)); - return (-1); - } - } - else { // specific IPv6 - - ip_version = 6; - if (inet_pton(AF_INET6, ip_addr, &sin6.sin6_addr) <= 0) { - syslog(LOG_WARNING, "Error binding to [%s] : %s\n", ip_addr, strerror(errno)); - return (-1); - } - } - - if (port_number == 0) { - syslog(LOG_WARNING, "Cannot start: no port number specified.\n"); - return (-1); - } - sin6.sin6_port = htons((u_short) port_number); - sin4.sin_port = htons((u_short) port_number); - - p = getprotobyname("tcp"); - - s = socket(((ip_version == 6) ? PF_INET6 : PF_INET), SOCK_STREAM, (p->p_proto)); - if (s < 0) { - if (IsDefault && (errno == EAFNOSUPPORT)) { - s = 0; - ip_addr = ipv4broadcast; - goto retry; - } - syslog(LOG_WARNING, "Can't create a listening socket: %s\n", strerror(errno)); - return (-1); - } - - // Set some socket options that make sense. - i = 1; - setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(i)); - - if (ip_version == 6) { - b = bind(s, (struct sockaddr *) &sin6, sizeof(sin6)); - } - else { - b = bind(s, (struct sockaddr *) &sin4, sizeof(sin4)); - } - - if (b < 0) { - syslog(LOG_ERR, "Can't bind: %s\n", strerror(errno)); - close(s); - return (-1); - } - - if (listen(s, queue_len) < 0) { - syslog(LOG_ERR, "Can't listen: %s\n", strerror(errno)); - close(s); - return (-1); - } - return (s); -} - - -// Create a Unix domain socket and listen on it -// sockpath - file name of the unix domain socket -// queue_len - Number of incoming connections to allow in the queue -int webcit_uds_server(char *sockpath, int queue_len) { - struct sockaddr_un addr; - int s; - int i; - int actual_queue_len; - - actual_queue_len = queue_len; - if (actual_queue_len < 5) - actual_queue_len = 5; - - i = unlink(sockpath); - if ((i != 0) && (errno != ENOENT)) { - syslog(LOG_WARNING, "webcit: can't unlink %s: %s", sockpath, strerror(errno)); - return (-1); - } - - memset(&addr, 0, sizeof(addr)); - addr.sun_family = AF_UNIX; - safestrncpy(addr.sun_path, sockpath, sizeof addr.sun_path); - - s = socket(AF_UNIX, SOCK_STREAM, 0); - if (s < 0) { - syslog(LOG_WARNING, "webcit: Can't create a unix domain socket: %s", strerror(errno)); - return (-1); - } - - if (bind(s, (struct sockaddr *) &addr, sizeof(addr)) < 0) { - syslog(LOG_WARNING, "webcit: Can't bind: %s", strerror(errno)); - close(s); - return (-1); - } - - if (listen(s, actual_queue_len) < 0) { - syslog(LOG_WARNING, "webcit: Can't listen: %s", strerror(errno)); - close(s); - return (-1); - } - - chmod(sockpath, 0777); - return (s); -} diff --git a/webcit-ng/text2html.c b/webcit-ng/text2html.c deleted file mode 100644 index 1a1bf1420..000000000 --- a/webcit-ng/text2html.c +++ /dev/null @@ -1,104 +0,0 @@ -// -// Convert text/plain to text/html -// -// Copyright (c) 2017-2022 by the citadel.org team -// -// This program is open source software. Use, duplication, or -// disclosure are subject to the GNU General Public License v3. - -#include "webcit.h" - - -// Convert a text/plain message to text/html -StrBuf *text2html(const char *supplied_charset, int treat_as_wiki, char *roomname, long msgnum, StrBuf *Source) { - StrBuf *sj = NULL; - - sj = NewStrBuf(); - if (!sj) { - return (sj); - } - - StrBufAppendPrintf(sj, "
");
-	StrEscAppend(sj, Source, NULL, 0, 0);	// FIXME - add code here to activate links
-	StrBufAppendPrintf(sj, "
\n"); - - return (sj); -} - - -// Convert a text/x-citadel-variformat message to text/html -StrBuf *variformat2html(StrBuf * Source) { - StrBuf *Target = NULL; - - Target = NewStrBuf(); - if (!Target) { - return (Target); - } - - const char *ptr, *pte; - const char *BufPtr = NULL; - StrBuf *Line = NewStrBufPlain(NULL, SIZ); - StrBuf *Line1 = NewStrBufPlain(NULL, SIZ); - StrBuf *Line2 = NewStrBufPlain(NULL, SIZ); - int bn = 0; - int bq = 0; - int i; - long len; - int intext = 0; - - if (StrLength(Source) > 0) - do { - StrBufSipLine(Line, Source, &BufPtr); - bq = 0; - i = 0; - ptr = ChrPtr(Line); - len = StrLength(Line); - pte = ptr + len; - - if ((intext == 1) && (isspace(*ptr))) { - StrBufAppendBufPlain(Target, HKEY("
"), 0); - } - intext = 1; - if (isspace(*ptr)) { - while ((ptr < pte) && ((*ptr == '>') || isspace(*ptr))) { - if (*ptr == '>') { - bq++; - } - ptr++; - i++; - } - } - - // Quoted text should be displayed in italics and in a - // different colour. This code understands Citadel-style - // " >" quotes and will convert to
tags. - if (i > 0) { - StrBufCutLeft(Line, i); - } - for (i = bn; i < bq; i++) { - StrBufAppendBufPlain(Target, HKEY("
"), 0); - } - for (i = bq; i < bn; i++) { - StrBufAppendBufPlain(Target, HKEY("
"), 0); - } - bn = bq; - - if (StrLength(Line) == 0) - continue; - - // Activate embedded URL's - UrlizeText(Line1, Line, Line2); - StrEscAppend(Target, Line1, NULL, 0, 0); - StrBufAppendBufPlain(Target, HKEY("\n"), 0); - } - while ((BufPtr != StrBufNOTNULL) && (BufPtr != NULL)); - - for (i = 0; i < bn; i++) { - StrBufAppendBufPlain(Target, HKEY("
"), 0); - } - StrBufAppendBufPlain(Target, HKEY("
\n"), 0); - FreeStrBuf(&Line); - FreeStrBuf(&Line1); - FreeStrBuf(&Line2); - return(Target); -} diff --git a/webcit-ng/tls.c b/webcit-ng/tls.c deleted file mode 100644 index c0e26f0d4..000000000 --- a/webcit-ng/tls.c +++ /dev/null @@ -1,207 +0,0 @@ -// Functions in this module handle SSL encryption when WebCit is running -// as an HTTPS server. -// -// Copyright (c) 1996-2022 by the citadel.org team -// -// This program is open source software. Use, duplication, or -// disclosure are subject to the GNU General Public License v3. - -#include "webcit.h" - -SSL_CTX *ssl_ctx; // global SSL context -char key_file[PATH_MAX] = ""; -char cert_file[PATH_MAX] = ""; -char *ssl_cipher_list = DEFAULT_SSL_CIPHER_LIST; - - -// Set the private key and certificate chain for the global SSL Context. -// This is called during initialization, and can be called again later if the certificate changes. -void bind_to_key_and_certificate(void) { - SSL_CTX *old_ctx, *new_ctx; - - if (!(new_ctx = SSL_CTX_new(SSLv23_server_method()))) { - syslog(LOG_WARNING, "SSL_CTX_new failed: %s", ERR_reason_error_string(ERR_get_error())); - return; - } - - syslog(LOG_INFO, "Requesting cipher list: %s", ssl_cipher_list); - if (!(SSL_CTX_set_cipher_list(new_ctx, ssl_cipher_list))) { - syslog(LOG_WARNING, "SSL_CTX_set_cipher_list failed: %s", ERR_reason_error_string(ERR_get_error())); - return; - } - - if (IsEmptyStr(key_file)) { - snprintf(key_file, sizeof key_file, "%s/keys/citadel.key", ctdl_dir); - } - if (IsEmptyStr(cert_file)) { - snprintf(cert_file, sizeof key_file, "%s/keys/citadel.cer", ctdl_dir); - } - - syslog(LOG_DEBUG, "crypto: [re]installing key \"%s\" and certificate \"%s\"", key_file, cert_file); - - SSL_CTX_use_certificate_chain_file(new_ctx, cert_file); - SSL_CTX_use_PrivateKey_file(new_ctx, key_file, SSL_FILETYPE_PEM); - - if ( !SSL_CTX_check_private_key(new_ctx) ) { - syslog(LOG_WARNING, "crypto: cannot install certificate: %s", ERR_reason_error_string(ERR_get_error())); - } - - old_ctx = ssl_ctx; - ssl_ctx = new_ctx; - sleep(1); - SSL_CTX_free(old_ctx); -} - - -// Initialize ssl engine, load certs and initialize openssl internals -void init_ssl(void) { - - // Initialize the OpenSSL library - SSL_load_error_strings(); - ERR_load_crypto_strings(); - OpenSSL_add_all_algorithms(); - SSL_library_init(); - - // Now try to bind to the key and certificate. - bind_to_key_and_certificate(); -} - - -// Check the modification time of the key and certificate -- reload if they changed -void update_key_and_cert_if_needed(void) { - static time_t previous_mtime = 0; - struct stat keystat; - struct stat certstat; - - if (stat(key_file, &keystat) != 0) { - syslog(LOG_ERR, "%s: %s", key_file, strerror(errno)); - return; - } - if (stat(cert_file, &certstat) != 0) { - syslog(LOG_ERR, "%s: %s", cert_file, strerror(errno)); - return; - } - - if ((keystat.st_mtime + certstat.st_mtime) != previous_mtime) { - bind_to_key_and_certificate(); - previous_mtime = keystat.st_mtime + certstat.st_mtime; - } -} - - -// starts SSL/TLS encryption for the current session. -void starttls(struct client_handle *ch) { - int retval, bits, alg_bits; - - if (!ssl_ctx) { - return; - } - - // Check the modification time of the key and certificate -- reload if they changed - update_key_and_cert_if_needed(); - - if (!(ch->ssl_handle = SSL_new(ssl_ctx))) { - syslog(LOG_WARNING, "SSL_new failed: %s", ERR_reason_error_string(ERR_get_error())); - return; - } - if (!(SSL_set_fd(ch->ssl_handle, ch->sock))) { - syslog(LOG_WARNING, "SSL_set_fd failed: %s", ERR_reason_error_string(ERR_get_error())); - SSL_free(ch->ssl_handle); - return; - } - retval = SSL_accept(ch->ssl_handle); - if (retval < 1) { - syslog(LOG_WARNING, "SSL_accept failed: %s", ERR_reason_error_string(ERR_get_error())); - } - else { - syslog(LOG_INFO, "SSL_accept success"); - } - bits = SSL_CIPHER_get_bits(SSL_get_current_cipher(ch->ssl_handle), &alg_bits); - syslog(LOG_INFO, "SSL/TLS using %s on %s (%d of %d bits)", - SSL_CIPHER_get_name(SSL_get_current_cipher(ch->ssl_handle)), - SSL_CIPHER_get_version(SSL_get_current_cipher(ch->ssl_handle)), bits, alg_bits); - syslog(LOG_INFO, "SSL started"); -} - - -// shuts down the TLS connection -void endtls(struct client_handle *ch) { - syslog(LOG_INFO, "Ending SSL/TLS"); - if (ch->ssl_handle != NULL) { - SSL_shutdown(ch->ssl_handle); - SSL_get_SSL_CTX(ch->ssl_handle); - SSL_free(ch->ssl_handle); - } - ch->ssl_handle = NULL; -} - - -// Send binary data to the client encrypted. -int client_write_ssl(struct client_handle *ch, char *buf, int nbytes) { - int retval; - int nremain; - char junk[1]; - - if (ch->ssl_handle == NULL) - return (-1); - - nremain = nbytes; - while (nremain > 0) { - if (SSL_want_write(ch->ssl_handle)) { - if ((SSL_read(ch->ssl_handle, junk, 0)) < 1) { - syslog(LOG_WARNING, "SSL_read in client_write: %s", ERR_reason_error_string(ERR_get_error())); - } - } - retval = SSL_write(ch->ssl_handle, &buf[nbytes - nremain], nremain); - if (retval < 1) { - long errval; - - errval = SSL_get_error(ch->ssl_handle, retval); - if (errval == SSL_ERROR_WANT_READ || errval == SSL_ERROR_WANT_WRITE) { - sleep(1); - continue; - } - syslog(LOG_WARNING, "SSL_write: %s", ERR_reason_error_string(ERR_get_error())); - if (retval == -1) { - syslog(LOG_WARNING, "errno is %d", errno); - endtls(ch); - } - return -1; - } - nremain -= retval; - } - return 0; -} - - -// read data from the encrypted layer -int client_read_ssl(struct client_handle *ch, char *buf, int nbytes) { - int bytes_read = 0; - int rlen = 0; - char junk[1]; - - if (ch->ssl_handle == NULL) - return (-1); - - while (bytes_read < nbytes) { - if (SSL_want_read(ch->ssl_handle)) { - if ((SSL_write(ch->ssl_handle, junk, 0)) < 1) { - syslog(LOG_WARNING, "SSL_write in client_read"); - } - } - rlen = SSL_read(ch->ssl_handle, &buf[bytes_read], nbytes - bytes_read); - if (rlen < 1) { - long errval; - errval = SSL_get_error(ch->ssl_handle, rlen); - if (errval == SSL_ERROR_WANT_READ || errval == SSL_ERROR_WANT_WRITE) { - sleep(1); - continue; - } - syslog(LOG_WARNING, "SSL_read error %ld", errval); - endtls(ch); - return (-1); - } - bytes_read += rlen; - } - return (bytes_read); -} diff --git a/webcit-ng/user_functions.c b/webcit-ng/user_functions.c deleted file mode 100644 index 0b4548ec3..000000000 --- a/webcit-ng/user_functions.c +++ /dev/null @@ -1,124 +0,0 @@ -// User functions -// -// Copyright (c) 1996-2022 by the citadel.org team -// -// This program is open source software. Use, duplication, or -// disclosure are subject to the GNU General Public License v3. - -#include "webcit.h" - - -// Fetch a user photo (avatar) -void fetch_user_photo(struct http_transaction *h, struct ctdlsession *c, char *username) { - char buf[1024]; - int content_length = 0; - char content_type[1024]; - char *image = NULL; - int actual_length = 0; - - ctdl_printf(c, "DLUI %s", username); - ctdl_readline(c, buf, sizeof(buf)); - if (buf[0] == '6') { - content_length = extract_int(&buf[4], 0); - extract_token(content_type, &buf[4], 3, '|', sizeof content_type); - - image = malloc(content_length); - if (image == NULL) { - do_502(h); - return; - } - actual_length = ctdl_read_binary(c, image, content_length); - - add_response_header(h, strdup("Content-type"), strdup(content_type)); - h->response_code = 200; - h->response_string = strdup("OK"); - h->response_body_length = actual_length; - h->response_body = image; - return; - } - - do_404(h); -} - - -// Fetch a user bio (profile) -void fetch_user_bio(struct http_transaction *h, struct ctdlsession *c, char *username) { - do_404(h); // FIXME finish this -} - - -// Client requested an object related to a user. -void object_in_user(struct http_transaction *h, struct ctdlsession *c, char *requested_username) { - char object_name[1024]; - - extract_token(object_name, h->url, 4, '/', sizeof object_name); - - if (!strcasecmp(object_name, "userpic")) { // user photo (avatar) - fetch_user_photo(h, c, requested_username); - return; - } - - if (!strcasecmp(object_name, "bio")) { // user bio (profile) - fetch_user_bio(h, c, requested_username); - return; - } - - do_404(h); // unknown object - return; -} - - -// Handle REST/DAV requests for the user itself (such as /ctdl/u/username -// or /ctdl/i/username/ but *not* specific properties of the user) -void the_user_itself(struct http_transaction *h, struct ctdlsession *c, char *username) { - do_404(h); -} - - -// Dispatcher for "/ctdl/u" and "/ctdl/u/" for the user list -void user_list(struct http_transaction *h, struct ctdlsession *c) { - do_404(h); -} - - -// Dispatcher for paths starting with /ctdl/u/ -void ctdl_u(struct http_transaction *h, struct ctdlsession *c) { - char requested_username[128]; - char buf[1024]; - - // All user-related functions require accessing the user in question. - extract_token(requested_username, h->url, 3, '/', sizeof requested_username); - unescape_input(requested_username); - - if (IsEmptyStr(requested_username)) { // /ctdl/u/ - user_list(h, c); - return; - } - - // At this point we have extracted the name of the user we're interested in. - // FIXME should we validate it? - - if (num_tokens(h->url, '/') == 4) { // /ctdl/u/username - the_user_itself(h, c, requested_username); - return; - } - - extract_token(buf, h->url, 4, '/', sizeof buf); - if (num_tokens(h->url, '/') == 5) { - if (IsEmptyStr(buf)) { - the_user_itself(h, c, requested_username); // /ctdl/u/username/ (same as /ctdl/u/username) - } - else { - object_in_user(h, c, requested_username); // /ctdl/u/username/object - } - return; - } - - if (num_tokens(h->url, '/') == 6) { - object_in_user(h, c, requested_username); // /ctdl/u/username/object/ or possibly /ctdl/u/username/object/component - return; - } - - // If we get to this point, the client requested an action we don't know how to perform. - do_404(h); -} diff --git a/webcit-ng/util.c b/webcit-ng/util.c deleted file mode 100644 index 72bf66038..000000000 --- a/webcit-ng/util.c +++ /dev/null @@ -1,97 +0,0 @@ -// -// Utility functions -// -// Copyright (c) 1996-2022 by the citadel.org team -// -// This program is open source software. Use, duplication, or -// disclosure are subject to the GNU General Public License v3. - -#include "webcit.h" - - -// remove escaped strings from i.e. the url string (like %20 for blanks) -int unescape_input(char *buf) { - unsigned int a, b; - char hex[3]; - long buflen; - long len; - - buflen = strlen(buf); - - while ((buflen > 0) && (isspace(buf[buflen - 1]))) { - buf[buflen - 1] = 0; - buflen--; - } - - a = 0; - while (a < buflen) { - if (buf[a] == '+') - buf[a] = ' '; - if (buf[a] == '%') { - // don't let % chars through, rather truncate the input. - if (a + 2 > buflen) { - buf[a] = '\0'; - buflen = a; - } - else { - hex[0] = buf[a + 1]; - hex[1] = buf[a + 2]; - hex[2] = 0; - b = 0; - b = decode_hex(hex); - buf[a] = (char) b; - len = buflen - a - 2; - if (len > 0) - memmove(&buf[a + 1], &buf[a + 3], len); - - buflen -= 2; - } - } - a++; - } - return a; -} - - -// Supplied with a unix timestamp, generate a textual time/date stamp. -// Caller owns the returned memory. -char *http_datestring(time_t xtime) { - - // HTTP Months - do not translate - these are not for human consumption - static char *httpdate_months[] = { - "Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" - }; - - // HTTP Weekdays - do not translate - these are not for human consumption - static char *httpdate_weekdays[] = { - "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" - }; - - struct tm t; - long offset; - char offsign; - int n = 40; - char *buf = malloc(n); - if (!buf) { - return (NULL); - } - - localtime_r(&xtime, &t); - - // Convert "seconds west of GMT" to "hours/minutes offset" - offset = t.tm_gmtoff; - if (offset > 0) { - offsign = '+'; - } - else { - offset = 0L - offset; - offsign = '-'; - } - offset = ((offset / 3600) * 100) + (offset % 60); - - snprintf(buf, n, "%s, %02d %s %04d %02d:%02d:%02d %c%04ld", - httpdate_weekdays[t.tm_wday], - t.tm_mday, httpdate_months[t.tm_mon], t.tm_year + 1900, t.tm_hour, t.tm_min, t.tm_sec, offsign, offset); - return (buf); -} diff --git a/webcit-ng/webcit.h b/webcit-ng/webcit.h deleted file mode 100644 index 9e6d069ba..000000000 --- a/webcit-ng/webcit.h +++ /dev/null @@ -1,165 +0,0 @@ -// webcit.h - "header of headers" -// -// Copyright (c) 1996-2022 by the citadel.org team -// -// This program is open source software. You can redistribute it and/or -// modify it under the terms of the GNU General Public License, version 3. - -#define SHOW_ME_VAPPEND_PRINTF - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#define OPENSSL_NO_KRB5 // Work around RedHat's b0rken OpenSSL includes -#include -#include -#include -#include -#define _(x) x // temporary hack until we add i18n back in -//#define DEBUG_HTTP // uncomment to debug HTTP headers - -// XML_StopParser is present in expat 2.x -#if XML_MAJOR_VERSION > 1 -#define HAVE_XML_STOPPARSER -#endif - -struct client_handle { // this gets passed up the stack from the webserver to the application code - int sock; - SSL *ssl_handle; -}; - -struct keyval { // key/value pair (for array) - char *key; - char *val; -}; - -struct http_transaction { // The lifetime of an HTTP request goes through this data structure. - char *method; // The top half is built up by the web server and sent up to the - char *url; // application stack. The second half is built up by the application - char *http_version; // stack and sent back down to the web server, which transmits it to - char *site_prefix; // the client. - Array *request_headers; - char *request_body; - long request_body_length; - Array *request_parms; // anything after the "?" in the URL - int response_code; - char *response_string; - Array *response_headers; - char *response_body; - long response_body_length; -}; - -#define AUTH_MAX 256 // Maximum length of an HTTP AUTH header or equivalent cookie data -struct ctdlsession { - struct ctdlsession *next; - int is_bound; // Nonzero if this record is currently bound to a running thread - int sock; // Socket connection to Citadel server - char auth[AUTH_MAX]; // Auth string (empty if not logged in) - char whoami[64]; // Display name of currently logged in user (empty if not logged in) - char room[128]; // What room we are currently in - int room_current_view; - int room_default_view; - int is_room_aide; // nonzero if the user has aide rights to THIS room - int can_delete_messages; // nonzeri if the user is permitted to delete messages in THIS room - long last_seen; - int new_messages; - int total_messages; - time_t last_access; // Timestamp of last request that used this session - time_t num_requests_handled; - time_t room_mtime; // Timestampt of the most recent write activity in this room - int new_mail; // number of new messages in the user's INBOX -}; - -extern char *ssl_cipher_list; -extern int is_https; // nonzero if we are an HTTPS server today -extern char *ctdl_dir; // directory where Citadel Server is running -void init_ssl(void); -void starttls(struct client_handle *); -void endtls(struct client_handle *); -int client_write_ssl(struct client_handle *ch, char *buf, int nbytes); -int client_read_ssl(struct client_handle *ch, char *buf, int nbytes); - -enum { - WEBSERVER_HTTP, - WEBSERVER_HTTPS, - WEBSERVER_UDS -}; - -#define TRACE syslog(LOG_DEBUG, "\033[3%dmCHECKPOINT: %s:%d\033[0m", ((__LINE__%6)+1), __FILE__, __LINE__) -#define SLEEPING 180 // TCP connection timeout -#define MAX_WORKER_THREADS 32 // Maximum number of worker threads permitted to exist -#define DEFAULT_SSL_CIPHER_LIST "DEFAULT" // See http://openssl.org/docs/apps/ciphers.html -#define WEBSERVER_PORT 80 -#define WEBSERVER_INTERFACE "*" -#define CTDL_DIR "/usr/local/citadel" -#define DEVELOPER_ID 0 -#define CLIENT_ID 4 -#define TARGET "webcit02" /* Window target for inline URL's */ - -void worker_entry(int *); -void perform_one_http_transaction(struct client_handle *ch); -void add_response_header(struct http_transaction *h, char *key, char *val); -void perform_request(struct http_transaction *); -void do_204(struct http_transaction *); -void do_404(struct http_transaction *); -void do_412(struct http_transaction *); -void do_502(struct http_transaction *); -void output_static(struct http_transaction *); -int uds_connectsock(char *); -void ctdl_a(struct http_transaction *, struct ctdlsession *); -void ctdl_f(struct http_transaction *, struct ctdlsession *); -void ctdl_r(struct http_transaction *, struct ctdlsession *); -void ctdl_u(struct http_transaction *, struct ctdlsession *); -struct ctdlsession *connect_to_citadel(struct http_transaction *); -void disconnect_from_citadel(struct ctdlsession *); -char *header_val(struct http_transaction *h, char *requested_header); -char *get_url_param(struct http_transaction *h, char *requested_param); -int unescape_input(char *); -void http_redirect(struct http_transaction *h, char *to_where); -char *http_datestring(time_t xtime); -long *get_msglist(struct ctdlsession *c, char *which_msgs); -void caldav_report(struct http_transaction *h, struct ctdlsession *c); -long locate_message_by_uid(struct ctdlsession *c, char *uid); -void ctdl_delete_msgs(struct ctdlsession *c, long *msgnums, int num_msgs); -void dav_delete_message(struct http_transaction *h, struct ctdlsession *c, long msgnum); -void dav_get_message(struct http_transaction *h, struct ctdlsession *c, long msgnum); -void dav_put_message(struct http_transaction *h, struct ctdlsession *c, char *euid, long old_msgnum); -ssize_t ctdl_write(struct ctdlsession *ctdl, const void *buf, size_t count); -int login_to_citadel(struct ctdlsession *c, char *auth, char *resultbuf); -StrBuf *ctdl_readtextmsg(struct ctdlsession *ctdl); -StrBuf *html2html(const char *supplied_charset, int treat_as_wiki, char *roomname, long msgnum, StrBuf *Source); -void download_mime_component(struct http_transaction *h, struct ctdlsession *c, long msgnum, char *partnum); -StrBuf *text2html(const char *supplied_charset, int treat_as_wiki, char *roomname, long msgnum, StrBuf *Source); -StrBuf *variformat2html(StrBuf *Source); -int ctdl_readline(struct ctdlsession *ctdl, char *buf, int maxbytes); -int ctdl_read_binary(struct ctdlsession *ctdl, char *buf, int bytes_requested); -void ctdl_c(struct http_transaction *h, struct ctdlsession *c); -int webserver(char *webserver_interface, int webserver_port, int webserver_protocol); -void ctdl_printf(struct ctdlsession *ctdl, const char *format,...); -int webcit_tcp_server(const char *ip_addr, int port_number, int queue_len); -void UrlizeText(StrBuf* Target, StrBuf *Source, StrBuf *WrkBuf); -void json_render_one_message(struct http_transaction *h, struct ctdlsession *c, long msgnum); diff --git a/webcit-ng/webserver.c b/webcit-ng/webserver.c deleted file mode 100644 index 2e22efabd..000000000 --- a/webcit-ng/webserver.c +++ /dev/null @@ -1,147 +0,0 @@ -// webserver.c -// -// This module handles the task of setting up a listening socket, accepting -// connections, and dispatching active connections onto a pool of worker -// threads. -// -// Copyright (c) 1996-2022 by the citadel.org team -// -// This program is open source software. Use, duplication, or -// disclosure are subject to the GNU General Public License v3. - -#include "webcit.h" // All other headers are included from this header. - -int num_threads_executing = 1; // Number of worker threads currently bound to a connected client -int num_threads_existing = 1; // Total number of worker threads that exist right now -int is_https = 0; // Set to nonzero if we are running as an HTTPS server today. -static void *original_brk = NULL; // Remember the original program break so we can test for leaks - - -// Spawn an additional worker thread into the pool. -void spawn_another_worker_thread(int *pointer_to_master_socket) { - pthread_t th; // Thread descriptor - pthread_attr_t attr; // Thread attributes - - // set attributes for the new thread - pthread_attr_init(&attr); - pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); - pthread_attr_setstacksize(&attr, 1048576); // Large stacks to prevent MIME parser crash on FreeBSD - - // now create the thread - if (pthread_create(&th, &attr, (void *(*)(void *)) worker_entry, (void *) pointer_to_master_socket) != 0) { - syslog(LOG_WARNING, "Can't create thread: %s", strerror(errno)); - } - else { - ++num_threads_existing; - ++num_threads_executing; - } - - // free up the attributes - pthread_attr_destroy(&attr); -} - - -// Entry point for worker threads -void worker_entry(int *pointer_to_master_socket) { - int master_socket = *pointer_to_master_socket; - int i = 0; - int fail_this_transaction = 0; - struct client_handle ch; - - while (1) { - // Each worker thread blocks on accept() while waiting for something to do. - // We don't have to worry about the "thundering herd" problem on modern kernels; for an explanation see - // https://stackoverflow.com/questions/2213779/does-the-thundering-herd-problem-exist-on-linux-anymore - memset(&ch, 0, sizeof ch); - ch.sock = -1; - errno = EAGAIN; - do { - --num_threads_executing; - syslog(LOG_DEBUG, "Additional memory allocated since startup: %d bytes", (int) (sbrk(0) - original_brk)); - syslog(LOG_DEBUG, "Thread 0x%x calling accept() on master socket %d", (unsigned int) pthread_self(), - master_socket); - ch.sock = accept(master_socket, NULL, 0); - if (ch.sock < 0) { - syslog(LOG_DEBUG, "accept() : %s", strerror(errno)); - } - ++num_threads_executing; - syslog(LOG_DEBUG, "socket %d is awake , threads executing: %d , threads total: %d", ch.sock, - num_threads_executing, num_threads_existing); - } while ((master_socket > 0) && (ch.sock < 0)); - - // If all threads are executing, spawn more, up to the maximum - if ((num_threads_executing >= num_threads_existing) && (num_threads_existing <= MAX_WORKER_THREADS)) { - spawn_another_worker_thread(pointer_to_master_socket); - } - - // We have a client. Do some work. - - // Set the SO_REUSEADDR socket option - i = 1; - setsockopt(ch.sock, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(i)); - - // If we are an HTTPS server, go crypto now. - if (is_https) { - starttls(&ch); - if (ch.ssl_handle == NULL) { - fail_this_transaction = 1; - } - } - else { - int fdflags; - fdflags = fcntl(ch.sock, F_GETFL); - if (fdflags < 0) { - syslog(LOG_WARNING, "unable to get server socket flags! %s", strerror(errno)); - } - } - - // Perform an HTTP transaction... - if (fail_this_transaction == 0) { - perform_one_http_transaction(&ch); - } - - // Shut down SSL/TLS if required... - if (is_https) { - endtls(&ch); - } - // ...and close the socket. - //syslog(LOG_DEBUG, "Closing socket %d...", ch.sock); - //lingering_close(ch.sock); - close(ch.sock); - syslog(LOG_DEBUG, "Closed socket %d.", ch.sock); - } -} - - -// Start up a TCP HTTP[S] server on the requested port -int webserver(char *webserver_interface, int webserver_port, int webserver_protocol) { - int master_socket = (-1); - original_brk = sbrk(0); - - switch (webserver_protocol) { - case WEBSERVER_HTTP: - syslog(LOG_DEBUG, "Starting HTTP server on %s:%d", webserver_interface, webserver_port); - master_socket = webcit_tcp_server(webserver_interface, webserver_port, 10); - break; - case WEBSERVER_HTTPS: - syslog(LOG_DEBUG, "Starting HTTPS server on %s:%d", webserver_interface, webserver_port); - master_socket = webcit_tcp_server(webserver_interface, webserver_port, 10); - init_ssl(); - is_https = 1; - break; - default: - syslog(LOG_ERR, "unknown protocol"); - ;; - } - - if (master_socket < 1) { - syslog(LOG_ERR, "Unable to bind the web server listening socket"); - return (1); - } - - syslog(LOG_INFO, "Listening on socket %d", master_socket); - signal(SIGPIPE, SIG_IGN); - - worker_entry(&master_socket); // this thread becomes a worker - return (0); -}