webcit-ng
authorArt Cancro <ajc@citadel.org>
Sun, 12 Mar 2017 05:32:38 +0000 (00:32 -0500)
committerArt Cancro <ajc@citadel.org>
Sun, 12 Mar 2017 05:32:38 +0000 (00:32 -0500)
28 files changed:
webcit-ng/Makefile [new file with mode: 0644]
webcit-ng/README.txt [new file with mode: 0644]
webcit-ng/admin_functions.c [new file with mode: 0644]
webcit-ng/api.txt [new file with mode: 0644]
webcit-ng/caldav_reports.c [new file with mode: 0644]
webcit-ng/ctdl_commands.c [new file with mode: 0644]
webcit-ng/ctdlclient.c [new file with mode: 0644]
webcit-ng/ctdlfunctions.c [new file with mode: 0644]
webcit-ng/html2html.c [new file with mode: 0644]
webcit-ng/http.c [new file with mode: 0644]
webcit-ng/main.c [new file with mode: 0644]
webcit-ng/messages.c [new file with mode: 0644]
webcit-ng/request.c [new file with mode: 0644]
webcit-ng/room_functions.c [new file with mode: 0644]
webcit-ng/ssl.c [new file with mode: 0644]
webcit-ng/static.c [new file with mode: 0644]
webcit-ng/static/citadel-logo.gif [new file with mode: 0644]
webcit-ng/static/index.html [new file with mode: 0644]
webcit-ng/static/js/login.js [new file with mode: 0644]
webcit-ng/static/js/main.js [new file with mode: 0644]
webcit-ng/static/js/views.js [new file with mode: 0644]
webcit-ng/static/throbber.gif [new file with mode: 0644]
webcit-ng/tcp_sockets.c [new file with mode: 0644]
webcit-ng/text2html.c [new file with mode: 0644]
webcit-ng/threaded_view.c [new file with mode: 0644]
webcit-ng/util.c [new file with mode: 0644]
webcit-ng/webcit.h [new file with mode: 0644]
webcit-ng/webserver.c [new file with mode: 0644]

diff --git a/webcit-ng/Makefile b/webcit-ng/Makefile
new file mode 100644 (file)
index 0000000..d1519e0
--- /dev/null
@@ -0,0 +1,25 @@
+OBJS := http.o main.o request.o ssl.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 threaded_view.o html2html.o text2html.o
+CFLAGS := -ggdb
+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
+       gcc -MM $(CFLAGS) $*.c > $*.d
+
+# remove compilation products
+clean:
+       rm -f webcit *.o *.d
+
+distclean: clean
+
+# install to target directory
+install:
+       echo Not yet...
diff --git a/webcit-ng/README.txt b/webcit-ng/README.txt
new file mode 100644 (file)
index 0000000..82c0a93
--- /dev/null
@@ -0,0 +1,31 @@
+
+
+This is WebCit-NG, a complete refactoring of the WebCit server that will
+focus on "REST first" and build around that.  The code will be well
+layered with as little spaghetti as possible.
+
+Please don't mess with this yet.  I'm only pushing it upstream so it gets
+backed up.
+
+
+DESIGN GOALS:
+-------------
+
+*      Hold as little state as possible
+
+*      Require NO cleanup.   Killing the process lets the OS reclaim all resources.
+
+*      As much as possible, resources should be freed by just coming back down the stack.
+
+*      Readability of the code is more important than shaving off a few CPU cycles.
+
+*      Throw sensitive data such as passwords back and forth in clear text.
+       If you want privacy, encrypt the whole session.  Anything else is false security.
+
+
+
+
+REST format URIs will generally take the form of:
+
+       /ctdl/objectClass/[container/]object[/operation]
+
diff --git a/webcit-ng/admin_functions.c b/webcit-ng/admin_functions.c
new file mode 100644 (file)
index 0000000..6071dca
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * Admin functions
+ *
+ * Copyright (c) 1996-2017 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.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+
+#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), 0);
+
+       syslog(LOG_DEBUG, "try_login(username='%s',password=(%d bytes))", username, 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
+
+       h->response_code = 200;                                                 // 'buf' will contain the relevant response
+       h->response_string = strdup("OK");
+       add_response_header(h, strdup("Content-type"), strdup("text/plain"));
+       h->response_body = strdup(buf);
+       h->response_body_length = strlen(h->response_body);
+}
+
+
+/*
+ * /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->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.
+
+       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->uri, "/ctdl/a/login")) {                             // log in
+               try_login(h, c);
+               return;         
+       }
+
+       if (!strcasecmp(h->uri, "/ctdl/a/logout")) {                            // log out
+               logout(h, c);
+               return;         
+       }
+
+       if (!strcasecmp(h->uri, "/ctdl/a/whoami")) {                            // return display name of user
+               whoami(h, c);
+               return;         
+       }
+
+       do_404(h);                                                              // unknown
+}
diff --git a/webcit-ng/api.txt b/webcit-ng/api.txt
new file mode 100644 (file)
index 0000000..f750caf
--- /dev/null
@@ -0,0 +1,12 @@
+
+Method         URI                             Function
+------         ------------------------------  -------------------------------------
+GET            /ctdl/a/landing.html            Site root redirects to here
+OPTIONS                /ctdl/r/ROOMNAME/               returns just what you'd expect
+PROPFIND       /ctdl/r/ROOMNAME/               Show a bunch of crap
+GET            /ctdl/r/ROOMNAME/               Returns information about the room (name, view, etc.) in JSON format
+GET            /ctdl/r/ROOMNAME/msgs.all       JSON array of message list in room
+GET            /ctdl/r/ROOMNAME/msgs.new       JSON array of message list in room (new messages)
+GET            /ctdl/c/info                    Returns a JSON representation of the output of an INFO server command
+POST           /ctdl/a/login
+GET            /ctdl/a/whoami
diff --git a/webcit-ng/caldav_reports.c b/webcit-ng/caldav_reports.c
new file mode 100644 (file)
index 0000000..b34581a
--- /dev/null
@@ -0,0 +1,301 @@
+/*
+ * This file contains functions which handle all of the CalDAV "REPORT" queries
+ * specified in RFC4791 section 7.
+ *
+ * Copyright (c) 2016 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.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+
+#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.
+ */
+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, "<D:response>");
+       StrBufAppendPrintf(ReportOut,   "<D:href>");
+       StrBufXMLEscAppend(ReportOut,   ThisHref, NULL, 0, 0);
+       StrBufAppendPrintf(ReportOut,   "</D:href>");
+       StrBufAppendPrintf(ReportOut,   "<D:propstat>");
+
+       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,   "<D:status>");
+               StrBufAppendPrintf(ReportOut,           "HTTP/1.1 200 OK");
+               StrBufAppendPrintf(ReportOut,   "</D:status>");
+               StrBufAppendPrintf(ReportOut,   "<D:prop>");
+               StrBufAppendPrintf(ReportOut,           "<D:getetag>");
+               StrBufAppendPrintf(ReportOut,                   "%ld", msgnum);
+               StrBufAppendPrintf(ReportOut,           "</D:getetag>");
+               StrBufAppendPrintf(ReportOut,           "<C:calendar-data>");
+               StrBufXMLEscAppend(ReportOut,                    Caldata, NULL, 0, 0);
+               StrBufAppendPrintf(ReportOut,           "</C:calendar-data>");
+               StrBufAppendPrintf(ReportOut,   "</D:prop>");
+               FreeStrBuf(&Caldata);
+               Caldata = NULL;
+       }
+       else {
+               // syslog(LOG_DEBUG, "caldav_response(%s) 404 not found", ChrPtr(ThisHref));
+               StrBufAppendPrintf(ReportOut,   "<D:status>");
+               StrBufAppendPrintf(ReportOut,           "HTTP/1.1 404 not found");
+               StrBufAppendPrintf(ReportOut,   "</D:status>");
+       }
+
+       StrBufAppendPrintf(ReportOut,   "</D:propstat>");
+       StrBufAppendPrintf(ReportOut, "</D:response>");
+}
+
+
+/*
+ * 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, "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
+               "<D:multistatus "
+                       "xmlns:D=\"DAV:\" "
+                       "xmlns:C=\"urn:ietf:params:xml:ns:caldav\""
+               ">"
+       );
+
+       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, "</D:multistatus>\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
new file mode 100644 (file)
index 0000000..6419355
--- /dev/null
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 1996-2016 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.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+
+#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->uri, "/ctdl/c/info")) {
+               serv_info(h, c);
+       }
+       else {
+               do_404(h);
+       }
+}
diff --git a/webcit-ng/ctdlclient.c b/webcit-ng/ctdlclient.c
new file mode 100644 (file)
index 0000000..b6f204a
--- /dev/null
@@ -0,0 +1,403 @@
+/*
+ * Functions that handle communication with a Citadel Server
+ *
+ * Copyright (c) 1987-2016 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.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+
+#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 newline-terminated line of text from the Citadel server.
+ * Implemented in terms of client_read() and is therefore transparent...
+ * 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;
+
+       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, "[ %s", buf);
+                       return(len);
+               }
+               ++len;
+       }
+       // syslog(LOG_DEBUG, "[ %s", 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)) >= 0) && (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, "] %s", 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;
+}
+
+
+/*
+ * TCP client - connect to a host/port 
+ */
+int tcp_connectsock(char *host, char *service)
+{
+       struct in6_addr serveraddr;
+       struct addrinfo hints;
+       struct addrinfo *res = NULL;
+       struct addrinfo *ai = NULL;
+       int rc = (-1);
+       int s = (-1);
+
+       if ((host == NULL) || IsEmptyStr(host))
+               return (-1);
+       if ((service == NULL) || IsEmptyStr(service))
+               return (-1);
+
+       syslog(LOG_DEBUG, "tcp_connectsock(%s,%s)", host, service);
+
+       memset(&hints, 0x00, sizeof(hints));
+       hints.ai_flags = AI_NUMERICSERV;
+       hints.ai_family = AF_UNSPEC;
+       hints.ai_socktype = SOCK_STREAM;
+
+       /*
+        * Handle numeric IPv4 and IPv6 addresses
+        */
+       rc = inet_pton(AF_INET, host, &serveraddr);
+       if (rc == 1) {                                          /* dotted quad */
+               hints.ai_family = AF_INET;
+               hints.ai_flags |= AI_NUMERICHOST;
+       } else {
+               rc = inet_pton(AF_INET6, host, &serveraddr);
+               if (rc == 1) {                                  /* IPv6 address */
+                       hints.ai_family = AF_INET6;
+                       hints.ai_flags |= AI_NUMERICHOST;
+               }
+       }
+
+       /* Begin the connection process */
+
+       rc = getaddrinfo(host, service, &hints, &res);
+       if (rc != 0) {
+               syslog(LOG_DEBUG, "%s: %s", host, gai_strerror(rc));
+               freeaddrinfo(res);
+               return(-1);
+       }
+
+       /*
+        * Try all available addresses until we connect to one or until we run out.
+        */
+       for (ai = res; ai != NULL; ai = ai->ai_next) {
+
+               if (ai->ai_family == AF_INET) syslog(LOG_DEBUG, "Trying IPv4");
+               else if (ai->ai_family == AF_INET6) syslog(LOG_DEBUG, "Trying IPv6");
+               else syslog(LOG_WARNING, "This is going to fail.");
+
+               s = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
+               if (s < 0) {
+                       syslog(LOG_WARNING, "socket() failed: %s", strerror(errno));
+                       freeaddrinfo(res);
+                       return(-1);
+               }
+               rc = connect(s, ai->ai_addr, ai->ai_addrlen);
+               if (rc >= 0) {
+                       int fdflags;
+                       freeaddrinfo(res);
+
+                       fdflags = fcntl(rc, F_GETFL);
+                       if (fdflags < 0) {
+                               syslog(LOG_ERR,
+                                      "unable to get socket %d flags! %s",
+                                      rc,
+                                      strerror(errno));
+                               close(rc);
+                               return -1;
+                       }
+                       fdflags = fdflags | O_NONBLOCK;
+                       if (fcntl(rc, F_SETFL, fdflags) < 0) {
+                               syslog(LOG_ERR,
+                                      "unable to set socket %d nonblocking flags! %s",
+                                      rc,
+                                      strerror(errno));
+                               close(s);
+                               return -1;
+                       }
+
+                       return(s);
+               }
+               else {
+                       syslog(LOG_WARNING, "connect() failed: %s", strerror(errno));
+                       close(s);
+               }
+       }
+        freeaddrinfo(res);
+       return(-1);
+}
+
+
+/*
+ * 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;
+       authbuf[0] = 0;
+
+       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, pwlen=%d", 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') {
+               strcpy(c->auth, auth);
+               extract_token(c->whoami, &buf[4], 0, '|', sizeof c->whoami);
+               syslog(LOG_DEBUG, "Login succeeded: %s", buf);
+               return(0);
+       }
+
+       syslog(LOG_DEBUG, "Login failed: %s", buf);
+       return(1);                                      // login failed; resultbuf will explain why
+}
+
+
+/*
+ * Hunt for, or create, a connection to our Citadel Server
+ */
+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);
+       syslog(LOG_DEBUG, "Session auth: %s", auth);            // remove this log when development is done
+
+       // 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);                                   // oh well
+       }
+
+       if (my_session->sock < 3) {
+               is_new_session = 1;
+       }
+       else {                                                  // make sure our Citadel session is still good
+               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, "");
+               my_session->sock = tcp_connectsock(ctdlhost, ctdlport);
+               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
new file mode 100644 (file)
index 0000000..9c16ded
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * These utility functions loosely make up a Citadel protocol client library.
+ *
+ * Copyright (c) 2016 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.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+
+#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/html2html.c b/webcit-ng/html2html.c
new file mode 100644 (file)
index 0000000..f7db44d
--- /dev/null
@@ -0,0 +1,669 @@
+/*
+ * Output an HTML message, modifying it slightly to make sure it plays nice
+ * with the rest of our web framework.
+ *
+ * Copyright (c) 2005-2017 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.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+
+#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       */
+
+       if (msg == NULL) {
+               return(NULL);
+       }
+
+       StrBuf *Target = NewStrBuf();
+       if (Target == NULL) {
+               return(NULL);
+       }
+
+       safestrncpy(charset, supplied_charset, sizeof charset);
+       sprintf(new_window, "<a target=\"%s\" href=", TARGET);
+
+       content_length = StrLength(Source);
+       msg = (char*) ChrPtr(Source);
+       buffer_length = content_length;
+
+       /* Do a first pass to isolate the message body */
+       ptr = msg + 1;
+       msgstart = msg;
+       msgend = &msg[content_length];
+
+       while (ptr < msgend) {
+
+               /* Advance to next tag */
+               ptr = strchr(ptr, '<');
+               if ((ptr == NULL) || (ptr >= msgend)) break;
+               ++ptr;
+               if ((ptr == NULL) || (ptr >= msgend)) break;
+
+               /*
+                *  Look for META tags.  Some messages (particularly in
+                *  Asian locales) illegally declare a message's character
+                *  set in the HTML instead of in the MIME headers.  This
+                *  is wrong but we have to work around it anyway.
+                */
+               if (!strncasecmp(ptr, "META", 4)) {
+
+                       char *meta_start;
+                       char *meta_end;
+                       int meta_length;
+                       char *meta;
+                       char *meta_http_equiv;
+                       char *meta_content;
+                       char *spaceptr;
+
+                       meta_start = &ptr[4];
+                       meta_end = strchr(ptr, '>');
+                       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/<msgno>/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 <BODY></BODY> 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<br>%s:%d", errno, strerror(errno), __FILE__, __LINE__);
+               goto BAIL;
+       }
+
+       if (BodyArea != NULL) {                                                 // Any attributes that were declared in the <body> tag
+               StrBufAppendBufPlain(converted_msg, HKEY("<div "), 0);          // are instead declared in this <div> 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, "<script", 7)) {
+                       if (scriptlevel == 0) {
+                               script_start_pos = StrLength(converted_msg);
+                       }
+                       ++scriptlevel;
+               }
+               if (!strncasecmp(ptr, "</script", 8)) {
+                       --scriptlevel;
+               }
+
+               /*
+                * Change mailto: links to WebCit mail, by replacing the
+                * link with one that points back to our mail room.  Due to
+                * the way we parse URL's, it'll even handle mailto: links
+                * that have "?subject=" in them.
+                */
+               if (!strncasecmp(ptr, "<a href=\"mailto:", 16)) {
+                       content_length += 64;
+                       StrBufAppendPrintf(converted_msg, "<a href=\"display_enter?force_room=_MAIL_?recp=");   // FIXME make compatible with webcit-ng
+                       ptr = &ptr[16];
+                       ++alevel;
+                       ++brak;
+               }
+               /* Make external links open in a separate window */
+               else if (!strncasecmp(ptr, "<a href=\"", 9)) {
+                       ++alevel;
+                       ++brak;
+                       if ( ((strchr(ptr, ':') < strchr(ptr, '/'))) &&  ((strchr(ptr, '/') < strchr(ptr, '>')))) {
+                               /* open external links to new window */
+                               StrBufAppendPrintf(converted_msg, new_window);
+                               ptr = &ptr[8];
+                       }
+                       else if (
+                               (treat_as_wiki)
+                               && (strncasecmp(ptr, "<a href=\"wiki?", 14))
+                               && (strncasecmp(ptr, "<a href=\"dotgoto?", 17))
+                               && (strncasecmp(ptr, "<a href=\"knrooms?", 17))
+                       ) {
+                               content_length += 64;
+                               StrBufAppendPrintf(converted_msg, "<a href=\"wiki?go=");
+                               //StrBufUrlescAppend(converted_msg, "FIXME ROOM NAME", NULL);                   // FIXME make compatible with webcit-ng
+                               StrBufAppendPrintf(converted_msg, "?page=");
+                               ptr = &ptr[9];
+                       }
+                       else {
+                               StrBufAppendPrintf(converted_msg, "<a href=\"");
+                               ptr = &ptr[9];
+                       }
+               }
+               /* Fixup <img src="cid:... ...> to fetch the mime part */
+               else if (!strncasecmp(ptr, "<img ", 5)) {
+                       char *cid_start, *cid_end;
+                       char* tag_end=strchr(ptr,'>');
+                       char* src;
+                       /* FIXME - handle this situation (maybe someone opened an <img cid... 
+                        * and then ended the message)
+                        */
+                       if (!tag_end) {
+                               syslog(LOG_DEBUG, "tag_end is null and ptr is:");
+                               syslog(LOG_DEBUG, "%s", ptr);
+                               syslog(LOG_DEBUG, "Theoretical bytes remaining: %d", (int)(msgend - ptr));
+                       }
+
+                       src=strstr(ptr, "src=\"cid:");
+                       ++brak;
+
+                       if (    src
+                               && isspace(*(src-1))
+                               && tag_end
+                               && (cid_start=strchr(src,':'))
+                               && (cid_end=strchr(cid_start,'"'))
+                               && (cid_end < tag_end)
+                       ) {
+                               /* copy tag and attributes up to src="cid: */
+                               StrBufAppendBufPlain(converted_msg, ptr, src - ptr, 0);
+                               cid_start++;
+
+                               /* add in /webcit/mimepart/<msgno>/CID/ 
+                                  trailing / stops dumb URL filters getting excited */
+                               StrBufAppendPrintf(converted_msg, " src=\"/ctdl/r/");
+                               StrBufXMLEscAppend(converted_msg, NULL, roomname, strlen(roomname), 0);
+                               syslog(LOG_DEBUG, "room name is '%s'", roomname);
+                               StrBufAppendPrintf(converted_msg, "/%ld/",msgnum);
+                               StrBufAppendBufPlain(converted_msg, cid_start, cid_end - cid_start, 0);
+                               StrBufAppendBufPlain(converted_msg, "\"", -1, 0);
+                               ptr = cid_end+1;
+                       }
+                       StrBufAppendBufPlain(converted_msg, ptr, tag_end - ptr, 0);
+                       ptr = tag_end;
+               }
+
+               /*
+                * Turn anything that looks like a URL into a real link, as long
+                * as it's not inside a tag already
+                */
+               else if ( (brak == 0) && (alevel == 0) &&
+                         ( (!strncasecmp(ptr, "http://", 7)) ||
+                           (!strncasecmp(ptr, "https://", 8)))) {
+                       /* Find the end of the link */
+                       int strlenptr;
+                       linklen = 0;
+                               
+                       strlenptr = strlen(ptr);
+                       for (i=0; i<=strlenptr; ++i) {
+                               if ((ptr[i]==0)
+                                   ||(isspace(ptr[i]))
+                                   ||(ptr[i]==10)
+                                   ||(ptr[i]==13)
+                                   ||(ptr[i]=='(')
+                                   ||(ptr[i]==')')
+                                   ||(ptr[i]=='<')
+                                   ||(ptr[i]=='>')
+                                   ||(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, "&nbsp;");
+                               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, "</a>");
+                       }
+               }
+               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, "</a>", 3)) --alevel;
+               }
+       }
+
+       if (BodyArea != NULL) {
+               StrBufAppendBufPlain(converted_msg, HKEY("</div>"), 0);                 // Close the div where we declared attributes copied
+               FreeStrBuf(&BodyArea);                                                  // from the original <body> 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, "<br>\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
new file mode 100644 (file)
index 0000000..eeb51cf
--- /dev/null
@@ -0,0 +1,284 @@
+/*
+ * This module handles HTTP transactions.
+ *
+ * Copyright (c) 1996-2016 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.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+
+#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;
+
+       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 http_header *new_response_header = malloc(sizeof(struct http_header));
+       new_response_header->key = key;
+       new_response_header->val = val;
+       new_response_header->next = h->response_headers;
+       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;
+       struct http_header *clh;                                                // general purpose iterator variable
+
+       memset(&h, 0, sizeof h);
+
+       while (len = client_readline(ch, buf, sizeof buf), (len > 0) ) {
+               ++lines_read;
+
+               if (lines_read == 1) {                                          // First line is method and URI.
+                       c = strchr(buf, ' ');                                   // IGnore the HTTP-version.
+                       if (c == NULL) {
+                               h.method = strdup("NULL");
+                               h.uri = strdup("/");
+                       }
+                       else {
+                               *c = 0;
+                               h.method = strdup(buf);
+                               ++c;
+                               d = c;
+                               c = strchr(d, ' ');
+                               if (c != NULL) {
+                                       *c = 0;
+                               }
+                               ++c;
+                               h.uri = 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 http_header *new_request_header = malloc(sizeof(struct http_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);
+                               new_request_header->next = h.request_headers;
+                               h.request_headers = new_request_header;
+#ifdef DEBUG_HTTP
+                               syslog(LOG_DEBUG, "{ %s: %s", new_request_header->key, new_request_header->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 {
+               syslog(LOG_DEBUG, "< %s %s", h.method, h.uri);
+
+               // 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(2, HKEY("\033[31m"));
+                       //write(2, h.request_body, h.request_body_length);
+                       //write(2, HKEY("\033[0m\n"));
+
+               }
+
+               // Now pass control up to the next layer to perform the request.
+               perform_request(&h);
+
+               // Output the results back to the client.
+               //write(2, HKEY("\033[32m"));
+               //write(2, h.response_body, h.response_body_length);
+               //write(2, HKEY("\033[0m\n"));
+
+               syslog(LOG_DEBUG, "> %03d %s", h.response_code, h.response_string);
+               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
+               for (clh = h.response_headers; clh != NULL; clh = clh->next) {
+#ifdef DEBUG_HTTP
+                       syslog(LOG_DEBUG, "} %s: %s", clh->key, clh->val);
+#endif
+                       client_printf(ch, "%s: %s\r\n", clh->key, clh->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 (h.request_headers) {
+               if (h.request_headers->key) free(h.request_headers->key);
+               if (h.request_headers->val) free(h.request_headers->val);
+               clh = h.request_headers->next;
+               free(h.request_headers);
+               h.request_headers = clh;
+       }
+       while (h.response_headers) {
+               if (h.response_headers->key) free(h.response_headers->key);
+               if (h.response_headers->val) free(h.response_headers->val);
+               clh = h.response_headers->next;
+               free(h.response_headers);
+               h.response_headers = clh;
+       }
+       if (h.method) free(h.method);
+       if (h.uri) free(h.uri);
+       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 http_header *clh;                                                // general purpose iterator variable
+       for (clh = h->request_headers; clh != NULL; clh = clh->next) {
+               if (!strcasecmp(clh->key, requested_header)) {
+                       return(clh->val);
+               }
+       }
+       return(NULL);
+}
diff --git a/webcit-ng/main.c b/webcit-ng/main.c
new file mode 100644 (file)
index 0000000..69f4eca
--- /dev/null
@@ -0,0 +1,139 @@
+/*
+ * Main entry point for the program.
+ *
+ * Copyright (c) 1996-2017 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.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+
+#include "webcit.h"            // All other headers are included from this header.
+
+char *ctdlhost = CTDLHOST;
+char *ctdlport = CTDLPORT;
+
+/*
+ * 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':
+                       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]"
+                               "[remotehost [remoteport]]\n");
+                       return 1;
+       }
+
+       if (optind < argc) {
+               ctdlhost = argv[optind];
+               if (++optind < argc)
+                       ctdlport = 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-2017 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(0, 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
new file mode 100644 (file)
index 0000000..3b9f096
--- /dev/null
@@ -0,0 +1,280 @@
+/*
+ * Message base functions
+ *
+ * Copyright (c) 1996-2016 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.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+
+#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;
+       }
+
+       ctdl_printf(c, "ENT0 1|||4|||1|");              // This protocol syntax will give us metadata back after upload
+       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;
+       }
+
+       content_type = header_val(h, "Content-type");
+       ctdl_printf(c, "Content-type: %s\r\n", (content_type ? content_type : "application/octet-stream"));
+       ctdl_printf(c, "\r\n");
+       ctdl_write(c, h->request_body, h->request_body_length);
+       if (h->request_body[h->request_body_length] != '\n') {
+               ctdl_printf(c, "\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", bytes, 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
new file mode 100644 (file)
index 0000000..186b2ac
--- /dev/null
@@ -0,0 +1,188 @@
+/*
+ * 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-2016 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.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+
+#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");
+}
+
+
+/*
+ * 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 URI.
+       // 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->uri)) {                               // 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->uri, "/")) && (!strcasecmp(h->method, "GET")) ) {
+               http_redirect(h, "/ctdl/s/index.html");
+               return;
+       }
+
+       // Legacy URI patterns (/readnew?gotoroom=xxx&start_reading_at=yyy) ...
+       // Direct room name (/my%20blog) ...
+
+       // Everything below this line is strictly REST URI patterns.
+
+       if (strncasecmp(h->uri, HKEY("/ctdl/"))) {              // Reject non-REST
+               do_404(h);
+               return;
+       }
+
+       if (!strncasecmp(h->uri, HKEY("/ctdl/s/"))) {           // Static content
+               output_static(h);
+               return;
+       }
+
+       if (h->uri[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 URI by path and send the request to the appropriate part of the program.
+       //
+       switch(h->uri[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 'r':                                       // /ctdl/r/ == RESTful path to rooms
+                       ctdl_r(h, c);
+                       break;
+               case 'u':                                       // /ctdl/u/ == RESTful path to users
+                       do_404(h);
+                       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);
+               free(exp);
+               add_response_header(h, strdup("Set-Cookie"), strdup(koekje));
+       }
+
+       // During 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
new file mode 100644 (file)
index 0000000..9db3e79
--- /dev/null
@@ -0,0 +1,592 @@
+/*
+ * Room functions
+ *
+ * Copyright (c) 1996-2016 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.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+
+#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, "*")) {
+                       return(1);                              // wildcard match
+               }
+               long tagmsgnum = atol(tag);
+               if ( (tagmsgnum > 0) && (tagmsgnum == msgnum) ) {       // match
+                       return(1);
+               }
+       }
+
+       return(0);                                              // no match
+}
+
+
+/*
+ * 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 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->uri, 4, '/', sizeof buf);
+
+       if (!strncasecmp(buf, "msgs.", 5)) {                    // Client is requesting a list of message numbers
+               json_msglist(h, c, &buf[5]);
+               return;
+       }
+
+       if (!strncasecmp(buf, "threads", 5)) {                  // Client is requesting a threaded view (still kind of fuzzy here)
+               threaded_view(h, c, &buf[5]);
+               return;
+       }
+
+       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 {
+               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);
+
+
+       /*
+        * Was the client actually requesting a specific component within the message?
+        */
+       if (num_tokens(h->uri, '/') == 6) {
+               extract_token(buf, h->uri, 5, '/', sizeof buf);
+               if (!IsEmptyStr(buf)) {
+                       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, "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
+               "<D:multistatus "
+                       "xmlns:D=\"DAV:\" "
+                       "xmlns:C=\"urn:ietf:params:xml:ns:caldav\""
+               ">"
+       );
+
+       /* Transmit the collection resource */
+       StrBufAppendPrintf(Buf, "<D:response>");
+       StrBufAppendPrintf(Buf, "<D:href>");
+       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, "</D:href>");
+
+       StrBufAppendPrintf(Buf, "<D:propstat>");
+       StrBufAppendPrintf(Buf, "<D:status>HTTP/1.1 200 OK</D:status>");
+       StrBufAppendPrintf(Buf, "<D:prop>");
+       StrBufAppendPrintf(Buf, "<D:displayname>");
+       StrBufXMLEscAppend(Buf, NULL, c->room, strlen(c->room), 0);
+       StrBufAppendPrintf(Buf, "</D:displayname>");
+
+       StrBufAppendPrintf(Buf, "<D:owner />");         // empty owner ought to be legal; see rfc3744 section 5.1
+
+       StrBufAppendPrintf(Buf, "<D:resourcetype><D:collection />");
+       switch(c->room_default_view) {
+               case VIEW_CALENDAR:
+                       StrBufAppendPrintf(Buf, "<C:calendar />");      // RFC4791 section 4.2
+                       break;
+       }
+       StrBufAppendPrintf(Buf, "</D:resourcetype>");
+
+       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, "<C:supported-calendar-component-set><C:comp name=\"VEVENT\"/></C:supported-calendar-component-set>");
+                       StrBufAppendPrintf(Buf, "<C:supported-calendar-data>");
+                       StrBufAppendPrintf(Buf,         "<C:calendar-data content-type=\"text/calendar\" version=\"2.0\"/>");
+                       StrBufAppendPrintf(Buf, "</C:supported-calendar-data>");
+                       enumerate_by_euid = 1;
+                       break;
+               case VIEW_TASKS:                        // RFC4791 section 5.2
+                       StrBufAppendPrintf(Buf, "<C:supported-calendar-component-set><C:comp name=\"VTODO\"/></C:supported-calendar-component-set>");
+                       StrBufAppendPrintf(Buf, "<C:supported-calendar-data>");
+                       StrBufAppendPrintf(Buf,         "<C:calendar-data content-type=\"text/calendar\" version=\"2.0\"/>");
+                       StrBufAppendPrintf(Buf, "</C:supported-calendar-data>");
+                       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" ?
+                       enumerate_by_euid = 1;
+                       break;
+       }
+
+
+       /* FIXME get the mtime
+       StrBufAppendPrintf(Buf, "<D:getlastmodified>");
+       escputs(datestring);
+       StrBufAppendPrintf(Buf, "</D:getlastmodified>");
+       */
+
+       StrBufAppendPrintf(Buf, "</D:prop>");
+       StrBufAppendPrintf(Buf, "</D:propstat>");
+       StrBufAppendPrintf(Buf, "</D:response>\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, "<D:response>");
+
+                               // Generate the 'href' tag for this message
+                               StrBufAppendPrintf(Buf, "<D:href>");
+                               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, "</D:href>");
+
+                               switch(c->room_default_view) {
+                                       case VIEW_CALENDAR:
+                                               StrBufAppendPrintf(Buf, "<D:getcontenttype>text/calendar; component=vevent</D:getcontenttype>");
+                                               break;
+                                       case VIEW_TASKS:
+                                               StrBufAppendPrintf(Buf, "<D:getcontenttype>text/calendar; component=vtodo</D:getcontenttype>");
+                                               break;
+                                       case VIEW_ADDRESSBOOK:
+                                               StrBufAppendPrintf(Buf, "<D:getcontenttype>text/x-vcard</D:getcontenttype>");
+                                               break;
+                               }
+
+                               StrBufAppendPrintf(Buf, "<D:propstat>");
+                               StrBufAppendPrintf(Buf, "<D:status>HTTP/1.1 200 OK</D:status>");
+                               StrBufAppendPrintf(Buf, "<D:prop>");
+                               if (timestamp > 0) {
+                                       char *datestring = http_datestring(timestamp);
+                                       if (datestring) {
+                                               StrBufAppendPrintf(Buf, "<D:getlastmodified>");
+                                               StrBufXMLEscAppend(Buf, NULL, datestring, strlen(datestring), 0);
+                                               StrBufAppendPrintf(Buf, "</D:getlastmodified>");
+                                               free(datestring);
+                                       }
+                                       if (enumerate_by_euid) {
+                                               StrBufAppendPrintf(Buf, "<D:getetag>\"%ld\"</D:getetag>", msglist[i]);
+                                       }
+                               }
+                               StrBufAppendPrintf(Buf, "</D:prop></D:propstat></D:response>\n");
+                               free(e);
+                       }
+                       free(msglist);
+               };
+       }
+       // END COLLECTION
+
+       StrBufAppendPrintf(Buf, "</D:multistatus>\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("new_messages"),   c->new_messages         ));
+       JsonObjectAppend(j, NewJsonNumber(      HKEY("total_messages"), c->total_messages       ));
+
+       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")) {
+
+               // 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));
+
+               int ra = extract_int(buf, 5);
+               JsonObjectAppend(jr, NewJsonBool( HKEY("known"), (ra && UA_KNOWN)));
+               JsonObjectAppend(jr, NewJsonBool( HKEY("hasnewmsgs"), (ra && UA_HASNEWMSGS)));
+
+               int floor = extract_int(buf, 2);
+               JsonObjectAppend(jr, NewJsonNumber( HKEY("floor"), floor));
+
+               int rorder = extract_int(buf, 3);
+               JsonObjectAppend(jr, NewJsonNumber( HKEY("rorder"), rorder));
+
+               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];
+
+       // All room-related functions require being "in" the room specified.  Are we in that room already?
+       extract_token(requested_roomname, h->uri, 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
+                        //     4       (int)CCC->room.QRflags          Various flags associated with this room.
+                        //     5       (long)CCC->room.QRhighest       The highest message number present in this room
+                        //     6       (long)vbuf.v_lastseen           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.
+                        //     8       (int)raideflag                  Nonzero if user is either Aide or a Room Aide in this room
+                        //     9       (int)newmailcount               The number of new Mail messages the user has
+                        //     10      (int)CCC->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.
+                        //     14      (int)CCC->room.QRflags2         More flags associated with this room
+                        //     15      (long)CCC->room.QRmtime         Timestamp of the last write activity in this room
+               }
+               else {
+                       do_404(h);
+                       return;
+               }
+       }
+
+       // At this point our Citadel client session is "in" the specified room.
+
+       if (num_tokens(h->uri, '/') == 4) {                     //      /ctdl/r/roomname
+               the_room_itself(h, c);
+               return;
+       }
+
+       extract_token(buf, h->uri, 4, '/', sizeof buf);
+       if (num_tokens(h->uri, '/') == 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->uri, '/') == 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/ssl.c b/webcit-ng/ssl.c
new file mode 100644 (file)
index 0000000..81764a2
--- /dev/null
@@ -0,0 +1,470 @@
+/*
+ * Functions in this module handle SSL encryption when WebCit is running
+ * as an HTTPS server.
+ *
+ * Copyright (c) 1996-2016 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.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+
+#include "webcit.h"
+
+SSL_CTX *ssl_ctx;              /* SSL context */
+pthread_mutex_t **SSLCritters; /* Things needing locking */
+char *ssl_cipher_list = DEFAULT_SSL_CIPHER_LIST;
+void ssl_lock(int mode, int n, const char *file, int line);
+
+
+/*
+ * OpenSSL wants a callback function to identify the currently running thread.
+ * Since we are a pthreads program, we convert the output of pthread_self() to a long.
+ */
+static unsigned long id_callback(void)
+{
+       return (unsigned long) pthread_self();
+}
+
+
+/*
+ * OpenSSL wants a callback function to set and clear various types of locks.
+ * Since we are a pthreads program, we use mutexes.
+ */
+void ssl_lock(int mode, int n, const char *file, int line)
+{
+       if (mode & CRYPTO_LOCK) {
+               pthread_mutex_lock(SSLCritters[n]);
+       }
+       else {
+               pthread_mutex_unlock(SSLCritters[n]);
+       }
+}
+
+
+/*
+ * Initialize ssl engine, load certs and initialize openssl internals
+ */
+void init_ssl(void)
+{
+       const SSL_METHOD *ssl_method;
+       RSA *rsa=NULL;
+       X509_REQ *req = NULL;
+       X509 *cer = NULL;
+       EVP_PKEY *pk = NULL;
+       EVP_PKEY *req_pkey = NULL;
+       X509_NAME *name = NULL;
+       FILE *fp;
+       char buf[SIZ];
+       int rv = 0;
+
+       if (!access("/var/run/egd-pool", F_OK)) {
+               RAND_egd("/var/run/egd-pool");
+       }
+
+       if (!RAND_status()) {
+               syslog(LOG_WARNING, "PRNG not adequately seeded, won't do SSL/TLS");
+               return;
+       }
+
+       SSLCritters = malloc(CRYPTO_num_locks() * sizeof(pthread_mutex_t *));
+       if (!SSLCritters) {
+               syslog(LOG_ERR, "citserver: can't allocate memory!!");
+               exit(1);
+       } else {
+               int a;
+
+               for (a = 0; a < CRYPTO_num_locks(); a++) {
+                       SSLCritters[a] = malloc(sizeof(pthread_mutex_t));
+                       if (!SSLCritters[a]) {
+                               syslog(LOG_INFO, "citserver: can't allocate memory!!");
+                               exit(1);
+                       }
+                       pthread_mutex_init(SSLCritters[a], NULL);
+               }
+       }
+
+       /*
+        * Initialize SSL transport layer
+        */
+       SSL_library_init();
+       SSL_load_error_strings();
+       ssl_method = SSLv23_server_method();
+       if (!(ssl_ctx = SSL_CTX_new(ssl_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(ssl_ctx, ssl_cipher_list))) {
+               syslog(LOG_WARNING, "SSL_CTX_set_cipher_list failed: %s", ERR_reason_error_string(ERR_get_error()));
+               return;
+       }
+
+       CRYPTO_set_locking_callback(ssl_lock);
+       CRYPTO_set_id_callback(id_callback);
+
+       /*
+        * Get our certificates in order.
+        * First, create the key/cert directory if it's not there already...
+        */
+       mkdir(CTDL_CRYPTO_DIR, 0700);
+
+       /*
+        * If we still don't have a private key, generate one.
+        */
+       if (access(CTDL_KEY_PATH, R_OK) != 0) {
+               syslog(LOG_INFO, "Generating RSA key pair.\n");
+               rsa = RSA_generate_key(2048,    /* modulus size */
+                                       65537,  /* exponent */
+                                       NULL,   /* no callback */
+                                       NULL    /* no callback */
+               );
+               if (rsa == NULL) {
+                       syslog(LOG_WARNING, "Key generation failed: %s", ERR_reason_error_string(ERR_get_error()));
+               }
+               if (rsa != NULL) {
+                       fp = fopen(CTDL_KEY_PATH, "w");
+                       if (fp != NULL) {
+                               chmod(CTDL_KEY_PATH, 0600);
+                               if (PEM_write_RSAPrivateKey(fp, /* the file */
+                                                       rsa,    /* the key */
+                                                       NULL,   /* no enc */
+                                                       NULL,   /* no passphr */
+                                                       0,      /* no passphr */
+                                                       NULL,   /* no callbk */
+                                                       NULL    /* no callbk */
+                               ) != 1) {
+                                       syslog(LOG_WARNING, "Cannot write key: %s", ERR_reason_error_string(ERR_get_error()));
+                                       unlink(CTDL_KEY_PATH);
+                               }
+                               fclose(fp);
+                       }
+                       else {
+                               syslog(LOG_WARNING, "Cannot write key: %s", CTDL_KEY_PATH);
+                               exit(1);
+                       }
+                       RSA_free(rsa);
+               }
+       }
+
+       /*
+        * If there is no certificate file on disk, we will be generating a self-signed certificate
+        * in the next step.  Therefore, if we have neither a CSR nor a certificate, generate
+        * the CSR in this step so that the next step may commence.
+        */
+       if ( (access(CTDL_CER_PATH, R_OK) != 0) && (access(CTDL_CSR_PATH, R_OK) != 0) ) {
+               syslog(LOG_INFO, "Generating a certificate signing request.");
+
+               /*
+                * Read our key from the file.  No, we don't just keep this
+                * in memory from the above key-generation function, because
+                * there is the possibility that the key was already on disk
+                * and we didn't just generate it now.
+                */
+               fp = fopen(CTDL_KEY_PATH, "r");
+               if (fp) {
+                       rsa = PEM_read_RSAPrivateKey(fp, NULL, NULL, NULL);
+                       fclose(fp);
+               }
+
+               if (rsa) {
+
+                       /* Create a public key from the private key */
+                       if (pk=EVP_PKEY_new(), pk != NULL) {
+                               EVP_PKEY_assign_RSA(pk, rsa);
+                               if (req = X509_REQ_new(), req != NULL) {
+                                       const char *env;
+                                       /* Set the public key */
+                                       X509_REQ_set_pubkey(req, pk);
+                                       X509_REQ_set_version(req, 0L);
+                                       name = X509_REQ_get_subject_name(req);
+                                       X509_NAME_add_entry_by_txt(
+                                               name, "O", MBSTRING_ASC,
+                                               (unsigned char*)"Citadel Server",
+                                               -1, -1, 0
+                                       );
+                                       X509_NAME_add_entry_by_txt(
+                                               name, "OU", MBSTRING_ASC,
+                                               (unsigned char*)"Default Certificate PLEASE CHANGE",
+                                               -1, -1, 0
+                                       );
+                                       X509_NAME_add_entry_by_txt(
+                                               name, "CN",
+                                               MBSTRING_ASC, 
+                                               (unsigned char*)"*",
+                                               -1, -1, 0
+                                       );
+                               
+                                       X509_REQ_set_subject_name(req, name);
+
+                                       /* Sign the CSR */
+                                       if (!X509_REQ_sign(req, pk, EVP_md5())) {
+                                               syslog(LOG_WARNING, "X509_REQ_sign(): error");
+                                       }
+                                       else {
+                                               /* Write it to disk. */ 
+                                               fp = fopen(CTDL_CSR_PATH, "w");
+                                               if (fp != NULL) {
+                                                       chmod(CTDL_CSR_PATH, 0600);
+                                                       PEM_write_X509_REQ(fp, req);
+                                                       fclose(fp);
+                                               }
+                                               else {
+                                                       syslog(LOG_WARNING, "Cannot write key: %s", CTDL_CSR_PATH);
+                                                       exit(1);
+                                               }
+                                       }
+
+                                       X509_REQ_free(req);
+                               }
+                       }
+
+                       RSA_free(rsa);
+               }
+
+               else {
+                       syslog(LOG_WARNING, "Unable to read private key.");
+               }
+       }
+
+
+       /*
+        * Generate a self-signed certificate if we don't have one.
+        */
+       if (access(CTDL_CER_PATH, R_OK) != 0) {
+               syslog(LOG_INFO, "Generating a self-signed certificate.");
+
+               /* Same deal as before: always read the key from disk because
+                * it may or may not have just been generated.
+                */
+               fp = fopen(CTDL_KEY_PATH, "r");
+               if (fp) {
+                       rsa = PEM_read_RSAPrivateKey(fp, NULL, NULL, NULL);
+                       fclose(fp);
+               }
+
+               /* This also holds true for the CSR. */
+               req = NULL;
+               cer = NULL;
+               pk = NULL;
+               if (rsa) {
+                       if (pk=EVP_PKEY_new(), pk != NULL) {
+                               EVP_PKEY_assign_RSA(pk, rsa);
+                       }
+
+                       fp = fopen(CTDL_CSR_PATH, "r");
+                       if (fp) {
+                               req = PEM_read_X509_REQ(fp, NULL, NULL, NULL);
+                               fclose(fp);
+                       }
+
+                       if (req) {
+                               if (cer = X509_new(), cer != NULL) {
+
+                                       ASN1_INTEGER_set(X509_get_serialNumber(cer), 0);
+                                       X509_set_issuer_name(cer, req->req_info->subject);
+                                       X509_set_subject_name(cer, req->req_info->subject);
+                                       X509_gmtime_adj(X509_get_notBefore(cer), 0);
+                                       X509_gmtime_adj(X509_get_notAfter(cer),(long)60*60*24*SIGN_DAYS);
+
+                                       req_pkey = X509_REQ_get_pubkey(req);
+                                       X509_set_pubkey(cer, req_pkey);
+                                       EVP_PKEY_free(req_pkey);
+                                       
+                                       /* Sign the cert */
+                                       if (!X509_sign(cer, pk, EVP_md5())) {
+                                               syslog(LOG_WARNING, "X509_sign(): error");
+                                       }
+                                       else {
+                                               /* Write it to disk. */ 
+                                               fp = fopen(CTDL_CER_PATH, "w");
+                                               if (fp != NULL) {
+                                                       chmod(CTDL_CER_PATH, 0600);
+                                                       PEM_write_X509(fp, cer);
+                                                       fclose(fp);
+                                               }
+                                               else {
+                                                       syslog(LOG_WARNING, "Cannot write key: %s", CTDL_CER_PATH);
+                                                       exit(1);
+                                               }
+                                       }
+                                       X509_free(cer);
+                               }
+                       }
+
+                       RSA_free(rsa);
+               }
+       }
+
+       /*
+        * Now try to bind to the key and certificate.
+        * Note that we use SSL_CTX_use_certificate_chain_file() which allows
+        * the certificate file to contain intermediate certificates.
+        */
+       SSL_CTX_use_certificate_chain_file(ssl_ctx, CTDL_CER_PATH);
+       SSL_CTX_use_PrivateKey_file(ssl_ctx, CTDL_KEY_PATH, SSL_FILETYPE_PEM);
+       if ( !SSL_CTX_check_private_key(ssl_ctx) ) {
+               syslog(LOG_WARNING, "Cannot install certificate: %s", ERR_reason_error_string(ERR_get_error()));
+       }
+       
+}
+
+
+/*
+ * starts SSL/TLS encryption for the current session.
+ */
+void starttls(struct client_handle *ch) {
+       int retval, bits, alg_bits;
+
+       if (!ssl_ctx) {
+               return;
+       }
+       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) {
+               long errval;
+               const char *ssl_error_reason = NULL;
+
+               errval = SSL_get_error(ch->ssl_handle, retval);
+               ssl_error_reason = ERR_reason_error_string(ERR_get_error());
+               if (ssl_error_reason == NULL) {
+                       syslog(LOG_WARNING, "SSL_accept failed: errval=%ld, retval=%d %s", errval, retval, strerror(errval));
+               }
+               else {
+                       syslog(LOG_WARNING, "SSL_accept failed: %s\n", ssl_error_reason);
+               }
+               sleep(1);
+               retval = SSL_accept(ch->ssl_handle);
+       }
+       if (retval < 1) {
+               long errval;
+               const char *ssl_error_reason = NULL;
+
+               errval = SSL_get_error(ch->ssl_handle, retval);
+               ssl_error_reason = ERR_reason_error_string(ERR_get_error());
+               if (ssl_error_reason == NULL) {
+                       syslog(LOG_WARNING, "SSL_accept failed: errval=%ld, retval=%d (%s)", errval, retval, strerror(errval));
+               }
+               else {
+                       syslog(LOG_WARNING, "SSL_accept failed: %s", ssl_error_reason);
+               }
+               SSL_free(ch->ssl_handle);
+               ch->ssl_handle = NULL;
+               return;
+       }
+       else {
+               syslog(LOG_INFO, "SSL_accept success");
+       }
+       BIO_set_close(ch->ssl_handle->rbio, BIO_NOCLOSE);
+       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 got error %ld, ret %d", errval, retval);
+                       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/static.c b/webcit-ng/static.c
new file mode 100644 (file)
index 0000000..5508aba
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * Output static content
+ *
+ * Copyright (c) 1996-2016 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.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+
+#include "webcit.h"
+
+
+/*
+ * Called from perform_request() to handle the /ctdl/s/ prefix -- always static content.
+ */
+void output_static(struct http_transaction *h)
+{
+       char filename[PATH_MAX];
+       struct stat statbuf;
+
+       snprintf(filename, sizeof filename, "static/%s", &h->uri[8]);
+
+       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) {
+               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/static/citadel-logo.gif b/webcit-ng/static/citadel-logo.gif
new file mode 100644 (file)
index 0000000..58611e8
Binary files /dev/null and b/webcit-ng/static/citadel-logo.gif differ
diff --git a/webcit-ng/static/index.html b/webcit-ng/static/index.html
new file mode 100644 (file)
index 0000000..8795317
--- /dev/null
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<html>
+<title>W3.CSS Template</title>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<link rel="stylesheet" href="https://www.w3schools.com/lib/w3.css">
+<link rel="stylesheet" href="https://www.w3schools.com/lib/w3-theme-w3schools.css">
+<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto">
+<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
+<style>
+html,body,h1,h2,h3,h4,h5,h6 {font-family: "Roboto", sans-serif}
+.w3-sidenav a,.w3-sidenav h4 {padding: 12px;}
+.w3-bar a {
+       padding-top: 12px;
+       padding-bottom: 12px;
+}
+</style>
+<body>
+
+<!-- Navbar -->
+<div id="navbar" class="w3-top">
+  <div class="w3-bar w3-theme w3-top w3-left-align w3-large">
+       <a class="w3-bar-item w3-button w3-opennav w3-right w3-hide-large w3-hover-white w3-large w3-theme-l1" href="javascript:void(0)" onclick="w3_open()"><i class="fa fa-bars"></i></a>
+       <span id="ctdl_banner_title" class="w3-bar-item w3-button w3-theme-l1">XXX</span>
+       <a href="#" class="w3-bar-item w3-button w3-hide-small w3-hover-white">Ungoto</a>
+       <a href="#" class="w3-bar-item w3-button w3-hide-small w3-hover-white">Read new</a>
+       <a href="#" class="w3-bar-item w3-button w3-hide-small w3-hover-white">Read all</a>
+       <a href="#" class="w3-bar-item w3-button w3-hide-small w3-hover-white">Enter</a>
+       <a href="#" class="w3-bar-item w3-button w3-hide-small w3-hide-medium w3-hover-white">Skip</a>
+       <a href="#" class="w3-bar-item w3-button w3-hide-small w3-hide-medium w3-hover-white">Goto</a>
+       <a href="#" id="lilo" class="w3-bar-item w3-button w3-hide-small w3-hover-white">Login</a>
+       <span id="current_user" class="w3-bar-item w3-button w3-hide-small w3-hide-medium w3-hover-white">XXX</span>
+
+  </div>
+</div>
+
+<!-- Sidenav -->
+<nav class="w3-sidenav w3-collapse w3-theme-l5 w3-animate-left" style="z-index:3;width:250px;margin-top:43px;" id="sidebar">
+  <a href="javascript:void(0)" onclick="w3_close()" class="w3-right w3-xlarge w3-padding-large w3-hover-black w3-hide-large" title="close menu">
+       <i class="fa fa-remove"></i>
+  </a>
+  <h4><b>Menu</b></h4>
+  <a href="#" class="w3-hover-black">Link</a>
+  <a href="#" class="w3-hover-black">Link</a>
+  <a href="#" class="w3-hover-black">Link</a>
+  <a href="#" class="w3-hover-black">Link</a>
+</nav>
+
+<!-- Overlay effect when opening sidenav on small screens -->
+<div class="w3-overlay w3-hide-large" onclick="w3_close()" style="cursor:pointer" title="close side menu" id="myOverlay"></div>
+
+<!-- Main content: shift it to the right by 250 pixels when the sidenav is visible -->
+<div id="main" class="w3-main" style="margin-left:250px">
+MAIN
+<!-- END MAIN -->
+</div>
+
+<script type="text/javascript" src="js/login.js"></script>
+<script type="text/javascript" src="js/main.js"></script>
+<script type="text/javascript" src="js/views.js"></script>
+<script>
+// Get the Sidenav
+var sidebar = document.getElementById("sidebar");
+
+// Get the DIV with overlay effect
+var overlayBg = document.getElementById("myOverlay");
+
+// Toggle between showing and hiding the sidenav, and add overlay effect
+function w3_open() {
+       if (sidebar.style.display === 'block') {
+               sidebar.style.display = 'none';
+               overlayBg.style.display = "none";
+       } else {
+               sidebar.style.display = 'block';
+               overlayBg.style.display = "block";
+       }
+}
+
+// Close the sidenav with the close button
+function w3_close() {
+       sidebar.style.display = "none";
+       overlayBg.style.display = "none";
+}
+
+alert('yup startup');
+ctdl_startup();
+</script>
+
+</body>
+</html>
+
+
diff --git a/webcit-ng/static/js/login.js b/webcit-ng/static/js/login.js
new file mode 100644 (file)
index 0000000..e6ab91d
--- /dev/null
@@ -0,0 +1,94 @@
+//
+// Copyright (c) 2016-2017 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.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+
+function display_login_screen(any_message)
+{
+       document.getElementById("main").innerHTML =
+               "Put the login screen here, dummary<br><br>" +
+               any_message + "<br><br>" +
+               _("User name:") + "<input type=\"text\" id=\"username\"><br>" +
+               _("Password:") + "<input type=\"password\" id=\"password\"><br>" +
+               "<a href=\"javascript:login_button()\">" + _("Log in") + "</a>"
+       ;
+
+       update_banner();
+}
+
+
+function logout()
+{
+       var request = new XMLHttpRequest();
+       request.open("GET", "/ctdl/a/logout", true);
+       request.onreadystatechange = function() {
+               login_result(this.responseText);
+       };
+       request.send();
+       request = null;
+}
+
+
+function login_button(username)
+{
+       parms = 
+               document.getElementById("username").value
+               + "|"
+               + document.getElementById("password").value
+               + "|"
+       ;
+
+       var request = new XMLHttpRequest();
+       request.open("POST", "/ctdl/a/login", true);
+       request.onreadystatechange = function() {
+               login_result(this.responseText);
+       };
+       request.send(parms);
+       request = null;
+}
+
+
+function login_result(data)
+{
+       if (data.substring(0,1) == "2") {
+               logged_in = 1;
+               current_user = data.substring(4).split("|")[0];
+               update_banner();
+               document.getElementById("main").innerHTML = "FIXME ok we are logged in as " + current_user + " ... " ;
+       }
+       else {
+               display_login_screen(data.substring(4));
+       }
+}
+
+
+// Detect whether the Citadel session is logged in as a user and update our internal variables accordingly.
+//
+function detect_logged_in()
+{
+       var request = new XMLHttpRequest();
+       request.open("GET", "/ctdl/a/whoami", true);
+       request.onreadystatechange = function() {
+               detect_logged_in_2(this.responseText);
+       };
+       request.send();
+       request = null;
+}
+function detect_logged_in_2(data)
+{
+       if (data.length > 0) {
+               logged_in = 1;
+               current_user = data;
+       }
+       else {
+               logged_in = 0;
+               current_user = _("Not logged in.");
+       }
+}
diff --git a/webcit-ng/static/js/main.js b/webcit-ng/static/js/main.js
new file mode 100644 (file)
index 0000000..31e7ad0
--- /dev/null
@@ -0,0 +1,201 @@
+//
+// Copyright (c) 2016-2017 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.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+
+var current_room = "_BASEROOM_";
+var new_messages = 0;
+var total_messages = 0;
+var default_view = 0;
+var current_view = 0;
+var logged_in = 0;
+var current_user = _("Not logged in.");
+var serv_info;
+
+
+// Placeholder for when we add i18n later
+function _(x) {
+       return x;
+}
+
+
+// Generate a random string of the specified length
+// Useful for generating one-time-use div names
+//
+function randomString(length) {
+       var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz'.split('');
+
+       if (! length) {
+               length = Math.floor(Math.random() * chars.length);
+       }
+
+       var str = '';
+       for (var i = 0; i < length; i++) {
+               str += chars[Math.floor(Math.random() * chars.length)];
+       }
+       return str;
+}
+
+
+// string escape for html display
+//
+function escapeHTML(text) {
+    'use strict';
+    return text.replace(/[\"&<>]/g, function (a) {
+        return {
+               '"': '&quot;',
+               '&': '&amp;',
+               '<': '&lt;',
+               '>': '&gt;'
+       }[a];
+    });
+}
+
+
+// string escape for html display
+//
+function escapeHTMLURI(text) {
+    'use strict';
+    return text.replace(/./g, function (a) {
+        return '%' + a.charCodeAt(0).toString(16);
+    });
+}
+
+
+// string escape for JavaScript string
+//
+function escapeJS(text) {
+    'use strict';
+    return text.replace(/[\"\']/g, function (a) {
+        return '\\' + a ;
+    });
+}
+
+
+// This is called at the very beginning of the main page load.
+//
+function ctdl_startup() {
+       var request = new XMLHttpRequest();
+       request.open("GET", "/ctdl/c/info", true);
+       request.onreadystatechange = function() {
+               if ((this.readyState === 4) && ((this.status / 100) == 2)) {
+                       ctdl_startup_2(JSON.parse(this.responseText));
+               }
+       };
+       request.send();
+       request = null;
+}
+
+// Continuation of ctdl_startup() after serv_info is retrieved
+//
+function ctdl_startup_2(data) {
+       serv_info = data;
+
+       if (data.serv_rev_level < 905) {
+               alert("Citadel server is too old, some functions may not work");
+       }
+
+       update_banner();
+
+       // for now, show a room list in the main div
+       gotoroom("_BASEROOM_");
+       display_room_list();
+}
+
+// Display a room list in the main div.
+//
+function display_room_list() {
+       document.getElementById("sidebar").innerHTML = "<img src=\"/ctdl/s/throbber.gif\" />" ;         // show throbber while loading
+
+       var request = new XMLHttpRequest();
+       request.open("GET", "/ctdl/r/", true);
+       request.onreadystatechange = function() {
+               if ((this.readyState === 4) && ((this.status / 100) == 2)) {
+                       display_room_list_renderer(JSON.parse(this.responseText));
+               }
+       };
+       request.send();
+       request = null;
+}
+
+// Renderer for display_room_list()
+//
+function display_room_list_renderer(data) {
+       data = data.sort(function(a,b) {
+               if (a.floor != b.floor) {
+                       return(a.floor - b.floor);
+               }
+               if (a.rorder != b.rorder) {
+                       return(a.rorder - b.rorder);
+               }
+               return(a.name < b.name);
+       });
+
+       new_sidebar_text = "<ul>" ;
+
+       for (var i in data) {
+               if (i > 0) {
+                       if (data[i].floor != data[i-1].floor) {
+                               new_sidebar_text = new_sidebar_text + "<li class=\"divider\"></li>" ;
+                       }
+               }
+               new_sidebar_text = new_sidebar_text +
+                       "<li>"
+                       + "<a href=\"javascript:gotoroom('" + escapeJS(escapeHTML(data[i].name)) + "');\">"
+                       + escapeHTML(data[i].name)
+                       + "</a></li>"
+               ;
+       }
+       new_sidebar_text = new_sidebar_text + "</ul>";
+       document.getElementById("sidebar").innerHTML = new_sidebar_text ;
+}
+
+// Update the "banner" div with all relevant info.
+//
+function update_banner() {
+       detect_logged_in();
+       if (current_room) {
+               document.getElementById("ctdl_banner_title").innerHTML = current_room;
+       }
+       else {
+               document.getElementById("ctdl_banner_title").innerHTML = serv_info.serv_humannode;
+       }
+       document.getElementById("current_user").innerHTML = current_user ;
+       if (logged_in) {
+               document.getElementById("lilo").innerHTML = "<a href=\"/ctdl/a/logout\">" + _("Log off") + "</a>" ;
+       }
+       else {
+               document.getElementById("lilo").innerHTML = "<a href=\"javascript:display_login_screen('')\">" + _("Log in") + "</a>" ;
+       }
+}
+
+
+// goto room
+//
+function gotoroom(roomname) {
+       var request = new XMLHttpRequest();
+       request.open("GET", "/ctdl/r/" + escapeHTMLURI(roomname) + "/", true);
+       request.onreadystatechange = function() {
+               if ((this.readyState === 4) && ((this.status / 100) == 2)) {
+                       gotoroom_2(JSON.parse(this.responseText));
+               }
+       };
+       request.send();
+       request = null;
+}
+function gotoroom_2(data) {
+       current_room = data.name;
+       new_messages = data.new_messages;
+       total_messages = data.total_messages;
+       current_view = data.current_view;
+       default_view = data.default_view;
+       update_banner();
+       render_room_view();
+}
diff --git a/webcit-ng/static/js/views.js b/webcit-ng/static/js/views.js
new file mode 100644 (file)
index 0000000..33da436
--- /dev/null
@@ -0,0 +1,71 @@
+//
+// Copyright (c) 2016-2017 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.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+
+// List of defined views shamelessly swiped from libcitadel headers
+//
+var views = {
+       VIEW_BBS                : 0,    /* Bulletin board view */
+       VIEW_MAILBOX            : 1,    /* Mailbox summary */
+       VIEW_ADDRESSBOOK        : 2,    /* Address book view */
+       VIEW_CALENDAR           : 3,    /* Calendar view */
+       VIEW_TASKS              : 4,    /* Tasks view */
+       VIEW_NOTES              : 5,    /* Notes view */
+       VIEW_WIKI               : 6,    /* Wiki view */
+       VIEW_CALBRIEF           : 7,    /* Brief Calendar view */
+       VIEW_JOURNAL            : 8,    /* Journal view */
+       VIEW_DRAFTS             : 9,    /* Drafts view */
+       VIEW_BLOG               : 10,   /* Blog view */
+       VIEW_QUEUE              : 11,   /* SMTP QUEUE rooms */
+       VIEW_WIKIMD             : 12,   /* Markdown Wiki view */
+};
+
+
+// This function is the dispatcher that determines the correct view for a room,
+// and calls the correct renderer.
+//
+function render_room_view() {
+
+       switch(current_view) {
+               case views.VIEW_MAILBOX:                                                // FIXME view mail rooms as forums for now
+               case views.VIEW_BBS:
+                       threads_readmessages();
+                       break;
+               default:
+                       document.getElementById("main").innerHTML = "The view for " + current_room + " is " + current_view + " but there is no renderer." ;
+                       break;
+       }
+
+}
+
+
+// bbs "threads" view
+// The inner div exists so that if the user clicks away early, the main div doesn't get clobbered when the load completes.
+//
+function threads_readmessages() {
+       var innerdivname = randomString(5);
+       document.getElementById("main").innerHTML = "<div id=\"" + innerdivname + "\"><img src=\"/ctdl/s/throbber.gif\" />" + _("Loading messages from server, please wait") + "</div>" ;
+
+       var request = new XMLHttpRequest();
+       request.open("GET", "/ctdl/r/" + escapeHTMLURI(current_room) + "/" + "threads", true);
+       request.onreadystatechange = function() {
+               if (this.readyState === 4) {
+                       if ((this.status / 100) == 2) {
+                               document.getElementById(innerdivname).outerHTML = this.responseText;
+                       }
+                       else {
+                               document.getElementById(innerdivname).outerHTML = "ERROR " + this.status ;
+                       }
+               }
+       };
+       request.send();
+       request = null;
+}
diff --git a/webcit-ng/static/throbber.gif b/webcit-ng/static/throbber.gif
new file mode 100644 (file)
index 0000000..772aa89
Binary files /dev/null and b/webcit-ng/static/throbber.gif differ
diff --git a/webcit-ng/tcp_sockets.c b/webcit-ng/tcp_sockets.c
new file mode 100644 (file)
index 0000000..55899bb
--- /dev/null
@@ -0,0 +1,201 @@
+/*
+ * TCP sockets layer
+ *
+ * Copyright (c) 1987-2016 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.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+
+#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
new file mode 100644 (file)
index 0000000..708d3e1
--- /dev/null
@@ -0,0 +1,122 @@
+/*
+ * Convert text/plain to text/html
+ *
+ * Copyright (c) 2017 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.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+
+#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, "<pre>");
+       StrEscAppend(sj, Source, NULL, 0, 0);           // FIXME - add code here to activate links
+       StrBufAppendPrintf(sj, "</pre>\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("<br>"), 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 <BLOCKQUOTE> tags.
+                        */
+                       if (i > 0) StrBufCutLeft(Line, i);
+
+                       for (i = bn; i < bq; i++)                               
+                               StrBufAppendBufPlain(Target, HKEY("<blockquote>"), 0);
+                       for (i = bq; i < bn; i++)                               
+                               StrBufAppendBufPlain(Target, HKEY("</blockquote>"), 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("</blockquote>"), 0);
+       }
+       StrBufAppendBufPlain(Target, HKEY("<br>\n"), 0);
+       FreeStrBuf(&Line);
+       FreeStrBuf(&Line1);
+       FreeStrBuf(&Line2);
+       return(Target);
+}
+
+
+
diff --git a/webcit-ng/threaded_view.c b/webcit-ng/threaded_view.c
new file mode 100644 (file)
index 0000000..07d689c
--- /dev/null
@@ -0,0 +1,220 @@
+/*
+ * Threaded message view
+ *
+ * Copyright (c) 1996-2017 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.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+
+#include "webcit.h"
+
+struct mthread {
+       long msgnum;
+       time_t datetime;
+       int threadhash;
+       int refhashes[10];
+       char from[64];
+       int parent;
+};
+
+
+// Renderer for one message in the threaded view
+void thread_render_one_message(struct ctdlsession *c, StrBuf *sj, long msgnum)
+{
+       StrBuf *raw_msg = NULL;
+       StrBuf *sanitized_msg = NULL;
+       char buf[1024];
+       char content_transfer_encoding[1024] = { 0 };
+       char content_type[1024] = { 0 };
+
+       ctdl_printf(c, "MSG4 %ld", msgnum);
+       ctdl_readline(c, buf, sizeof(buf));
+       if (buf[0] == '1') {
+               while ( (ctdl_readline(c, buf, sizeof(buf)) >= 0) && (strcmp(buf, "text")) && (strcmp(buf, "000")) ) {
+                       // citadel header parsing here
+                       if (!strncasecmp(buf, "from=", 5)) {
+                               StrBufAppendPrintf(sj, "<b>From %s</b><br>", &buf[5]);  // FIXME that was temporary
+                       }
+                       if (!strncasecmp(buf, "part=", 5)) {
+                               //StrBufAppendPrintf(sj, "MIME part: %s<br>", &buf[5]); // FIXME that was temporary
+                       }
+               }
+               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);
+                                       //StrBufAppendPrintf(sj, "Content-type: %s<br>", content_type); // FIXME that was temporary
+                               }
+                       }
+                       raw_msg = ctdl_readtextmsg(c);
+               }
+               else {
+                       raw_msg = NULL;
+               }
+               if (raw_msg) {
+                       if (!strcasecmp(content_transfer_encoding, "base64")) {
+                               StrBufDecodeBase64(raw_msg);
+                       }
+                       if (!strcasecmp(content_transfer_encoding, "quoted-printable")) {
+                               StrBufDecodeQP(raw_msg);
+                       }
+                       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("<i>No renderer for this content type</i><br>"));
+                       }
+                       //sanitized_msg = NewStrBufDup(raw_msg);
+                       FreeStrBuf(&raw_msg);
+                       if (sanitized_msg) {
+                               StrBufAppendBuf(sj, sanitized_msg, 0);
+                               FreeStrBuf(&sanitized_msg);
+                       }
+               }
+       }
+}
+
+
+void thread_o_print(struct ctdlsession *c, StrBuf *sj, struct mthread *m, int num_msgs, int where_parent_is, int nesting_level)
+{
+       int i = 0;
+       int j = 0;
+       int num_printed = 0;
+
+       for (i=0; i<num_msgs; ++i) {
+               if (m[i].parent == where_parent_is) {
+
+                       if (++num_printed == 1) {
+                               StrBufAppendPrintf(sj, "<ul>");
+                               StrBufAppendPrintf(sj, "<table style=\"border: 1px solid black;\"><tr><td>");   // temporary, for visualization
+                       }
+
+                       //for (j=nesting_level; j>0; --j) {
+                               //StrBufAppendPrintf(sj, " ");
+                       //}
+
+                       StrBufAppendPrintf(sj, "<li class=\"post\" id=\"post-%ld\">",  m[i].msgnum);
+                       thread_render_one_message(c, sj, m[i].msgnum);
+                       StrBufAppendPrintf(sj, "</li>\r\n");
+                       if (i != 0) {
+                               thread_o_print(c, sj, m, num_msgs, i, nesting_level+1);
+                       }
+               }
+       }
+
+       if (num_printed > 0) {
+               StrBufAppendPrintf(sj, "</td></tr></table>");           // temporary, for visualization
+               StrBufAppendPrintf(sj, "</ul>");
+       }
+}
+
+
+
+void threaded_view(struct http_transaction *h, struct ctdlsession *c, char *which)
+{
+       int num_msgs = 0;
+       int num_alloc = 0;
+       struct mthread *m;
+       char buf[1024];
+       char refs[1024];
+       int i, j, k;
+
+       ctdl_printf(c, "MSGS ALL|||9");                 // 9 == headers + thread references
+       ctdl_readline(c, buf, sizeof(buf));
+       if (buf[0] != '1') {
+               do_404(h);
+               return;
+       }
+
+       StrBuf *sj = NewStrBuf();
+       StrBufAppendPrintf(sj, "<html><body>\r\n");
+
+       while (ctdl_readline(c, buf, sizeof buf), strcmp(buf,"000")) {
+
+               ++num_msgs;
+               if (num_msgs > num_alloc) {
+                       if (num_alloc == 0) {
+                               num_alloc = 100;
+                               m = malloc(num_alloc * sizeof(struct mthread));
+                       }
+                       else {
+                               num_alloc *= 2;
+                               m = realloc(m, (num_alloc * sizeof(struct mthread)));
+                       }
+               }
+
+               memset(&m[num_msgs-1], 0, sizeof(struct mthread));
+               m[num_msgs-1].msgnum = extract_long(buf, 0);
+               m[num_msgs-1].datetime = extract_long(buf, 1);
+               extract_token(m[num_msgs-1].from, buf, 2, '|', sizeof m[num_msgs-1].from);
+               m[num_msgs-1].threadhash = extract_int(buf, 6);
+               extract_token(refs, buf, 7, '|', sizeof refs);
+
+               char *t;
+               char *r = refs;
+               i = 0;
+               while ((t = strtok_r(r, ",", &r))) {
+                       if (i == 0) {
+                               m[num_msgs-1].refhashes[0] = atoi(t);           // always keep the first one
+                       }
+                       else {
+                               memcpy(&m[num_msgs-1].refhashes[1], &m[num_msgs-1].refhashes[2], sizeof(int)*8 );       // shift the rest
+                               m[num_msgs-1].refhashes[9] = atoi(t);
+                       }
+                       ++i;
+               }
+
+       }
+
+       // Sort by thread.  I did read jwz's sorting algorithm and it looks pretty good, but jwz is a self-righteous asshole so we do it our way.
+       for (i=0; i<num_msgs; ++i) {
+               for (j=9; (j>=0)&&(m[i].parent==0); --j) {
+                       for (k=0; (k<num_msgs)&&(m[i].parent==0); ++k) {
+                               if (m[i].refhashes[j] == m[k].threadhash) {
+                                       m[i].parent = k;
+                               }
+                       }
+               }
+       }
+
+       // Now render it
+       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
+       thread_o_print(c, sj, m, num_msgs, 0, 0);               // Render threads recursively and recursively
+
+       // haha h0h0
+       if (num_msgs > 0) {
+               free(m);
+       }
+
+       StrBufAppendPrintf(sj, "</body></html>\r\n");
+
+       add_response_header(h, strdup("Content-type"), strdup("text/html; charset=utf-8"));
+       h->response_code = 200;
+       h->response_string = strdup("OK");
+       h->response_body_length = StrLength(sj);
+       h->response_body = SmashStrBuf(&sj);
+       return;
+}
+
+
diff --git a/webcit-ng/util.c b/webcit-ng/util.c
new file mode 100644 (file)
index 0000000..9fdf58a
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * Utility functions
+ *
+ * Copyright (c) 1996-2016 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.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+
+#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
new file mode 100644 (file)
index 0000000..6302d45
--- /dev/null
@@ -0,0 +1,158 @@
+/*
+ * webcit.h - "header of headers"
+ *
+ * Copyright (c) 1996-2017 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.
+ */
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <ctype.h>
+#include <syslog.h>
+#include <string.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <netdb.h>
+#include <sys/un.h>
+#include <sys/poll.h>
+#include <string.h>
+#include <pwd.h>
+#include <errno.h>
+#include <stdarg.h>
+#include <pthread.h>
+#include <signal.h>
+#include <syslog.h>
+#include <stdarg.h>
+#include <limits.h>
+#include <iconv.h>
+#include <libcitadel.h>
+#define OPENSSL_NO_KRB5                                // Work around RedHat's b0rken OpenSSL includes
+#include <openssl/ssl.h>
+#include <openssl/err.h>
+#include <openssl/rand.h>
+#include <expat.h>
+#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 http_header {                           // request and response headers in struct http_transaction use this format
+       struct http_header *next;
+       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 *uri;                              // 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.
+       struct http_header *request_headers;
+       char *request_body;
+       long request_body_length;
+       int response_code;
+       char *response_string;
+       struct http_header *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 new_messages;
+       int total_messages;
+       time_t last_access;                     // Timestamp of last request that used this session
+       time_t num_requests_handled;
+};
+
+extern char *ssl_cipher_list;
+extern int is_https;                           // nonzero if we are an HTTPS server today
+extern char *ctdlhost;
+extern char *ctdlport;
+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        CTDL_CRYPTO_DIR         "keys"
+#define CTDL_KEY_PATH          CTDL_CRYPTO_DIR "/webcit.key"
+#define CTDL_CSR_PATH          CTDL_CRYPTO_DIR "/webcit.csr"
+#define CTDL_CER_PATH          CTDL_CRYPTO_DIR "/webcit.cer"
+#define SIGN_DAYS              3650            // how long our certificate should live
+#define DEFAULT_SSL_CIPHER_LIST "DEFAULT"       // See http://openssl.org/docs/apps/ciphers.html
+#define WEBSERVER_PORT         80
+#define WEBSERVER_INTERFACE    "*"
+#define CTDLHOST               "uncensored.citadel.org"
+#define CTDLPORT               "504"
+#define DEVELOPER_ID           0
+#define CLIENT_ID              4
+#define TARGET                 "webcit01"      /* Window target for inline URL's */
+
+void worker_entry(int *pointer_to_master_socket);
+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_404(struct http_transaction *);
+void output_static(struct http_transaction *);
+int uds_connectsock(char *sockpath);
+int tcp_connectsock(char *host, char *service);
+void ctdl_a(struct http_transaction *, struct ctdlsession *);
+void ctdl_r(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);
+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);
+void threaded_view(struct http_transaction *h, struct ctdlsession *c, char *which);
+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);
+
+void *mallok(size_t size);
+void phree(void *ptr);
+void *reallok(void *ptr, size_t size);
+
diff --git a/webcit-ng/webserver.c b/webcit-ng/webserver.c
new file mode 100644 (file)
index 0000000..1a0776c
--- /dev/null
@@ -0,0 +1,160 @@
+/*
+ * 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-2016 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.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+
+#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. */
+               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);
+}