/* http_ischedule.c -- Routines for handling iSchedule in httpd
*
* Copyright (c) 1994-2012 Carnegie Mellon University. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The name "Carnegie Mellon University" must not be used to
* endorse or promote products derived from this software without
* prior written permission. For permission or any legal
* details, please contact
* Carnegie Mellon University
* Center for Technology Transfer and Enterprise Creation
* 4615 Forbes Avenue
* Suite 302
* Pittsburgh, PA 15213
* (412) 268-7393, fax: (412) 268-7395
* innovation@andrew.cmu.edu
*
* 4. Redistributions of any form whatsoever must retain the following
* acknowledgment:
* "This product includes software developed by Computing Services
* at Carnegie Mellon University (http://www.cmu.edu/computing/)."
*
* CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
* THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
* FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
#include <config.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <errno.h>
#include <sysexits.h>
#include <syslog.h>
#include <libical/ical.h>
#include "global.h"
#include "httpd.h"
#include "http_caldav_sched.h"
#include "http_dav.h"
#include "http_proxy.h"
#include "jcal.h"
#include "map.h"
#include "proxy.h"
#include "tok.h"
#include "util.h"
#include "xmalloc.h"
#include "xcal.h"
#include "xstrlcpy.h"
/* generated headers are not necessarily in current directory */
#include "imap/http_err.h"
#include <sasl/saslutil.h>
#define ISCHED_WELLKNOWN_URI "/.well-known/ischedule"
#ifdef WITH_DKIM
#include <dkim.h>
//#define TEST
#define BASE64_LEN(inlen) ((((inlen) + 2) / 3) * 4)
static DKIM_LIB *dkim_lib = NULL;
static struct buf privkey = BUF_INITIALIZER;
static struct buf tmpbuf = BUF_INITIALIZER;
static struct buf b64req = BUF_INITIALIZER;
#endif /* WITH_DKIM */
static void isched_init(struct buf *serverinfo);
static void isched_shutdown(void);
static int meth_get_isched(struct transaction_t *txn, void *params);
static int meth_options_isched(struct transaction_t *txn, void *params);
static int meth_post_isched(struct transaction_t *txn, void *params);
static int dkim_auth(struct transaction_t *txn);
static int meth_get_domainkey(struct transaction_t *txn, void *params);
static time_t compile_time;
static struct mime_type_t isched_mime_types[] = {
/* First item MUST be the default type and storage format */
{ "text/calendar; charset=utf-8", "2.0", "ics",
(struct buf* (*)(void *)) &my_icalcomponent_as_ical_string,
(void * (*)(const struct buf*)) &ical_string_as_icalcomponent,
(void (*)(void *)) &icalcomponent_free, NULL, NULL
},
{ "application/calendar+xml; charset=utf-8", NULL, "xcs",
(struct buf* (*)(void *)) &icalcomponent_as_xcal_string,
(void * (*)(const struct buf*)) &xcal_string_as_icalcomponent,
NULL, NULL, NULL
},
{ "application/calendar+json; charset=utf-8", NULL, "jcs",
(struct buf* (*)(void *)) &icalcomponent_as_jcal_string,
(void * (*)(const struct buf*)) &jcal_string_as_icalcomponent,
NULL, NULL, NULL,
},
{ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }
};
struct namespace_t namespace_ischedule = {
URL_NS_ISCHEDULE, 0, "ischedule", "/ischedule", ISCHED_WELLKNOWN_URI,
http_allow_noauth, /*authschemes*/0,
/*mbtype*/0,
(ALLOW_READ | ALLOW_POST | ALLOW_ISCHEDULE),
isched_init, NULL, NULL, isched_shutdown, NULL, NULL,
{
{ NULL, NULL }, /* ACL */
{ NULL, NULL }, /* BIND */
{ NULL, NULL }, /* CONNECT */
{ NULL, NULL }, /* COPY */
{ NULL, NULL }, /* DELETE */
{ &meth_get_isched, NULL }, /* GET */
{ &meth_get_isched, NULL }, /* HEAD */
{ NULL, NULL }, /* LOCK */
{ NULL, NULL }, /* MKCALENDAR */
{ NULL, NULL }, /* MKCOL */
{ NULL, NULL }, /* MOVE */
{ &meth_options_isched, NULL }, /* OPTIONS */
{ NULL, NULL }, /* PATCH */
{ &meth_post_isched, NULL }, /* POST */
{ NULL, NULL }, /* PROPFIND */
{ NULL, NULL }, /* PROPPATCH */
{ NULL, NULL }, /* PUT */
{ NULL, NULL }, /* REPORT */
{ &meth_trace, NULL }, /* TRACE */
{ NULL, NULL }, /* UNBIND */
{ NULL, NULL } /* UNLOCK */
}
};
struct namespace_t namespace_domainkey = {
URL_NS_DOMAINKEY, 0, "domainkey", "/domainkeys", "/.well-known/domainkey",
http_allow_noauth, /*authschemes*/0,
/*mbtype*/0,
ALLOW_READ,
NULL, NULL, NULL, NULL, NULL, NULL,
{
{ NULL, NULL }, /* ACL */
{ NULL, NULL }, /* BIND */
{ NULL, NULL }, /* CONNECT */
{ NULL, NULL }, /* COPY */
{ NULL, NULL }, /* DELETE */
{ &meth_get_domainkey, NULL }, /* GET */
{ &meth_get_domainkey, NULL }, /* HEAD */
{ NULL, NULL }, /* LOCK */
{ NULL, NULL }, /* MKCALENDAR */
{ NULL, NULL }, /* MKCOL */
{ NULL, NULL }, /* MOVE */
{ &meth_options, NULL }, /* OPTIONS */
{ NULL, NULL }, /* POST */
{ NULL, NULL }, /* PROPFIND */
{ NULL, NULL }, /* PROPPATCH */
{ NULL, NULL }, /* PUT */
{ NULL, NULL }, /* REPORT */
{ &meth_trace, NULL }, /* TRACE */
{ NULL, NULL }, /* UNBIND */
{ NULL, NULL } /* UNLOCK */
}
};
void isched_capa_hdr(struct transaction_t *txn, time_t *lastmod, struct stat *sb)
{
struct stat sbuf;
time_t mtime;
stat(config_filename, &sbuf);
mtime = MAX(compile_time, sbuf.st_mtime);
txn->resp_body.iserial = mtime +
(rscale_calendars ? rscale_calendars->num_elements : 0);
if (lastmod) *lastmod = mtime;
if (sb) *sb = sbuf;
}
/* iSchedule Receiver Capabilities */
static int meth_get_isched(struct transaction_t *txn,
void *params __attribute__((unused)))
{
int precond;
struct strlist *action;
static struct message_guid prev_guid;
struct message_guid guid;
const char *etag;
static time_t lastmod = 0;
struct stat sbuf;
static xmlChar *buf = NULL;
static int bufsiz = 0;
/* Initialize */
if (!lastmod) message_guid_set_null(&prev_guid);
/* Fill in iSchedule-Capabilities */
isched_capa_hdr(txn, &lastmod, &sbuf);
/* We don't handle GET on a anything other than ?action=capabilities */
action = hash_lookup("action", &txn->req_qparams);
if (!action || action->next || strcmp(action->s, "capabilities")) {
txn->error.desc = "Invalid action";
return HTTP_BAD_REQUEST;
}
/* Generate ETag based on compile date/time of this source file,
the number of available RSCALEs and the config file size/mtime */
assert(!buf_len(&txn->buf));
buf_printf(&txn->buf, "%ld-%zu-%ld-%ld", (long) compile_time,
rscale_calendars ? (size_t)rscale_calendars->num_elements : 0,
sbuf.st_mtime, sbuf.st_size);
message_guid_generate(&guid, buf_cstring(&txn->buf), buf_len(&txn->buf));
etag = message_guid_encode(&guid);
/* Check any preconditions, including range request */
txn->flags.ranges = 1;
precond = check_precond(txn, etag, lastmod);
switch (precond) {
case HTTP_OK:
case HTTP_PARTIAL:
case HTTP_NOT_MODIFIED:
/* Fill in Etag, Last-Modified, and Expires */
txn->resp_body.etag = etag;
txn->resp_body.lastmod = lastmod;
txn->resp_body.maxage = 86400; /* 24 hrs */
txn->flags.cc |= CC_MAXAGE;
if (precond != HTTP_NOT_MODIFIED) break;
GCC_FALLTHROUGH
default:
/* We failed a precondition - don't perform the request */
return precond;
}
if (!message_guid_equal(&prev_guid, &guid)) {
xmlNodePtr root, capa, node, comp, meth;
xmlNsPtr ns[NUM_NAMESPACE];
struct mime_type_t *mime;
struct icaltimetype date;
icaltimezone *utc = icaltimezone_get_utc_timezone();
int i, n, maxlen;
/* Start construction of our query-result */
if (!(root = init_xml_response("query-result", NS_ISCHED, NULL, ns))) {
txn->error.desc = "Unable to create XML response";
return HTTP_SERVER_ERROR;
}
capa = xmlNewChild(root, NULL, BAD_CAST "capabilities", NULL);
buf_reset(&txn->buf);
buf_printf(&txn->buf, "%ld", txn->resp_body.iserial);
xmlNewChild(capa, NULL, BAD_CAST "serial-number",
BAD_CAST buf_cstring(&txn->buf));
node = xmlNewChild(capa, NULL, BAD_CAST "versions", NULL);
xmlNewChild(node, NULL, BAD_CAST "version", BAD_CAST "1.0");
node = xmlNewChild(capa, NULL,
BAD_CAST "scheduling-messages", NULL);
comp = xmlNewChild(node, NULL, BAD_CAST "component", NULL);
xmlNewProp(comp, BAD_CAST "name", BAD_CAST "VEVENT");
meth = xmlNewChild(comp, NULL, BAD_CAST "method", NULL);
xmlNewProp(meth, BAD_CAST "name", BAD_CAST "REQUEST");
meth = xmlNewChild(comp, NULL, BAD_CAST "method", NULL);
xmlNewProp(meth, BAD_CAST "name", BAD_CAST "REPLY");
meth = xmlNewChild(comp, NULL, BAD_CAST "method", NULL);
xmlNewProp(meth, BAD_CAST "name", BAD_CAST "CANCEL");
comp = xmlNewChild(node, NULL, BAD_CAST "component", NULL);
xmlNewProp(comp, BAD_CAST "name", BAD_CAST "VTODO");
meth = xmlNewChild(comp, NULL, BAD_CAST "method", NULL);
xmlNewProp(meth, BAD_CAST "name", BAD_CAST "REQUEST");
meth = xmlNewChild(comp, NULL, BAD_CAST "method", NULL);
xmlNewProp(meth, BAD_CAST "name", BAD_CAST "REPLY");
meth = xmlNewChild(comp, NULL, BAD_CAST "method", NULL);
xmlNewProp(meth, BAD_CAST "name", BAD_CAST "CANCEL");
comp = xmlNewChild(node, NULL, BAD_CAST "component", NULL);
xmlNewProp(comp, BAD_CAST "name", BAD_CAST "VPOLL");
meth = xmlNewChild(comp, NULL, BAD_CAST "method", NULL);
xmlNewProp(meth, BAD_CAST "name", BAD_CAST "POLLSTATUS");
meth = xmlNewChild(comp, NULL, BAD_CAST "method", NULL);
xmlNewProp(meth, BAD_CAST "name", BAD_CAST "REQUEST");
meth = xmlNewChild(comp, NULL, BAD_CAST "method", NULL);
xmlNewProp(meth, BAD_CAST "name", BAD_CAST "REPLY");
meth = xmlNewChild(comp, NULL, BAD_CAST "method", NULL);
xmlNewProp(meth, BAD_CAST "name", BAD_CAST "CANCEL");
comp = xmlNewChild(node, NULL, BAD_CAST "component", NULL);
xmlNewProp(comp, BAD_CAST "name", BAD_CAST "VFREEBUSY");
meth = xmlNewChild(comp, NULL, BAD_CAST "method", NULL);
xmlNewProp(meth, BAD_CAST "name", BAD_CAST "REQUEST");
node = xmlNewChild(capa, NULL,
BAD_CAST "calendar-data-types", NULL);
for (mime = isched_mime_types; mime->content_type; mime++) {
xmlNodePtr type = xmlNewChild(node, NULL,
BAD_CAST "calendar-data-type", NULL);
/* Trim any charset from content-type */
buf_reset(&txn->buf);
buf_printf(&txn->buf, "%.*s",
(int) strcspn(mime->content_type, ";"),
mime->content_type);
xmlNewProp(type, BAD_CAST "content-type",
BAD_CAST buf_cstring(&txn->buf));
if (mime->version)
xmlNewProp(type, BAD_CAST "version", BAD_CAST mime->version);
}
node = xmlNewChild(capa, NULL, BAD_CAST "attachments", NULL);
xmlNewChild(node, NULL, BAD_CAST "inline", NULL);
node = xmlNewChild(capa, NULL, BAD_CAST "rscales", NULL);
if (rscale_calendars) {
for (i = 0, n = rscale_calendars->num_elements; i < n; i++) {
const char **rscale = icalarray_element_at(rscale_calendars, i);
xmlNewChild(node, NULL, BAD_CAST "rscale", BAD_CAST *rscale);
}
}
maxlen = config_getint(IMAPOPT_MAXMESSAGESIZE);
if (!maxlen) maxlen = INT_MAX;
buf_reset(&txn->buf);
buf_printf(&txn->buf, "%d", maxlen);
xmlNewChild(capa, NULL, BAD_CAST "max-content-length",
BAD_CAST buf_cstring(&txn->buf));
date = icaltime_from_timet_with_zone(caldav_epoch, 0, utc);
xmlNewChild(capa, NULL, BAD_CAST "min-date-time",
BAD_CAST icaltime_as_ical_string(date));
date = icaltime_from_timet_with_zone(caldav_eternity, 0, utc);
xmlNewChild(capa, NULL, BAD_CAST "max-date-time",
BAD_CAST icaltime_as_ical_string(date));
/* XXX need to fill these with values */
xmlNewChild(capa, NULL, BAD_CAST "max-recipients", NULL);
xmlNewChild(capa, NULL, BAD_CAST "administrator", NULL);
/* Dump XML response tree into a text buffer */
xmlDocDumpFormatMemoryEnc(root->doc, &buf, &bufsiz, "utf-8", 1);
xmlFreeDoc(root->doc);
if (!buf) {
txn->error.desc = "Error dumping XML tree";
return HTTP_SERVER_ERROR;
}
message_guid_copy(&prev_guid, &guid);
}
/* Output the XML response */
txn->resp_body.type = "application/xml; charset=utf-8";
write_body(precond, txn, (char *) buf, bufsiz);
xmlFree(buf);
return 0;
}
static int meth_options_isched(struct transaction_t *txn, void *params)
{
/* Fill in iSchedule-Capabilities */
isched_capa_hdr(txn, NULL, NULL);
return meth_options(txn, params);
}
/* iSchedule Receiver */
static int meth_post_isched(struct transaction_t *txn,
void *params __attribute__((unused)))
{
int ret = 0, r, authd = 0;
const char **hdr, **recipients;
struct mime_type_t *mime = NULL;
icalcomponent *ical = NULL, *comp;
icalcomponent_kind kind = 0;
icalproperty_method meth = 0;
icalproperty *prop = NULL;
const char *uid = NULL;
/* Fill in iSchedule-Capabilities */
isched_capa_hdr(txn, NULL, NULL);
/* Response should not be cached */
txn->flags.cc |= CC_NOCACHE;
/* Check iSchedule-Version */
if (!(hdr = spool_getheader(txn->req_hdrs, "iSchedule-Version")) ||
strcmp(hdr[0], "1.0")) {
txn->error.precond = ISCHED_UNSUPP_VERSION;
return HTTP_BAD_REQUEST;
}
/* Check Content-Type */
if ((hdr = spool_getheader(txn->req_hdrs, "Content-Type"))) {
for (mime = isched_mime_types; mime->content_type; mime++) {
if (is_mediatype(mime->content_type, hdr[0])) break;
}
}
if (!mime || !mime->content_type) {
txn->error.precond = ISCHED_UNSUPP_DATA;
return HTTP_BAD_REQUEST;
}
/* Check Originator */
if (!(hdr = spool_getheader(txn->req_hdrs, "Originator"))) {
txn->error.precond = ISCHED_ORIG_MISSING;
return HTTP_BAD_REQUEST;
}
else if (hdr[1]) {
/* Multiple Originators */
txn->error.precond = ISCHED_MULTIPLE_ORIG;
return HTTP_BAD_REQUEST;
}
/* Check Recipients */
if (!(recipients = spool_getheader(txn->req_hdrs, "Recipient"))) {
txn->error.precond = ISCHED_RECIP_MISSING;
return HTTP_BAD_REQUEST;
}
/* Read body */
txn->req_body.flags |= BODY_DECODE;
r = http_read_req_body(txn);
if (r) {
txn->flags.conn = CONN_CLOSE;
return r;
}
/* Make sure we have a body */
if (!buf_len(&txn->req_body.payload)) {
txn->error.desc = "Missing request body\r\n";
return HTTP_BAD_REQUEST;
}
/* Check authorization */
if (httpd_userid &&
config_mupdate_server && config_getstring(IMAPOPT_PROXYSERVERS)) {
/* Allow frontends to HTTP auth to backends and use iSchedule */
authd = 1;
}
else if (!spool_getheader(txn->req_hdrs, "DKIM-Signature")) {
if (!config_getswitch(IMAPOPT_ISCHEDULE_DKIM_REQUIRED)) authd = 1;
else txn->error.desc = "No signature";
}
else {
authd = dkim_auth(txn);
}
if (!authd) {
ret = HTTP_FORBIDDEN;
txn->error.precond = ISCHED_VERIFICATION_FAILED;
goto done;
}
/* Parse the iCal data for important properties */
ical = mime->to_object(&txn->req_body.payload);
if (!ical || (icalcomponent_isa(ical) != ICAL_VCALENDAR_COMPONENT)) {
txn->error.precond = ISCHED_INVALID_DATA;
return HTTP_BAD_REQUEST;
}
icalrestriction_check(ical);
if ((txn->error.desc = get_icalcomponent_errstr(ical))) {
assert(!buf_len(&txn->buf));
buf_setcstr(&txn->buf, txn->error.desc);
txn->error.desc = buf_cstring(&txn->buf);
txn->error.precond = ISCHED_INVALID_DATA;
return HTTP_BAD_REQUEST;
}
meth = icalcomponent_get_method(ical);
comp = icalcomponent_get_first_real_component(ical);
if (comp) {
uid = icalcomponent_get_uid(comp);
kind = icalcomponent_isa(comp);
prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
}
/* Check method preconditions */
if (!meth || !comp || !uid || !prop) {
txn->error.precond = ISCHED_INVALID_SCHED;
ret = HTTP_BAD_REQUEST;
goto done;
}
switch (kind) {
case ICAL_VFREEBUSY_COMPONENT:
if (meth == ICAL_METHOD_REQUEST)
ret = sched_busytime_query(txn, mime, ical);
else goto invalid_meth;
break;
case ICAL_VEVENT_COMPONENT:
case ICAL_VTODO_COMPONENT:
case ICAL_VPOLL_COMPONENT:
switch (meth) {
case ICAL_METHOD_POLLSTATUS:
if (kind != ICAL_VPOLL_COMPONENT) goto invalid_meth;
GCC_FALLTHROUGH
case ICAL_METHOD_REQUEST:
case ICAL_METHOD_REPLY:
case ICAL_METHOD_CANCEL: {
struct sched_data sched_data =
{ 1, meth == ICAL_METHOD_REPLY, 0,
ical, NULL, NULL, ICAL_SCHEDULEFORCESEND_NONE, NULL, NULL };
xmlNodePtr root = NULL;
xmlNsPtr ns[NUM_NAMESPACE];
struct auth_state *authstate;
int i;
/* Start construction of our schedule-response */
if (!(root = init_xml_response("schedule-response",
NS_ISCHED, NULL, ns))) {
ret = HTTP_SERVER_ERROR;
txn->error.desc = "Unable to create XML response\r\n";
goto done;
}
authstate = auth_newstate("anonymous");
/* Process each recipient */
for (i = 0; recipients[i]; i++) {
tok_t tok =
TOK_INITIALIZER(recipients[i], ",", TOK_TRIMLEFT|TOK_TRIMRIGHT);
char *recipient;
while ((recipient = tok_next(&tok))) {
/* Is recipient remote or local? */
struct caldav_sched_param sparam;
int r = caladdress_lookup(recipient, &sparam, /*schedule_addresses*/NULL);
/* Don't allow scheduling with remote users via iSchedule */
if (sparam.flags & SCHEDTYPE_REMOTE) r = HTTP_FORBIDDEN;
sched_param_fini(&sparam);
if (r) sched_data.status = REQSTAT_NOUSER;
else sched_deliver(httpd_userid, recipient, &sched_data, authstate);
xml_add_schedresponse(root, NULL, BAD_CAST recipient,
BAD_CAST sched_data.status);
}
tok_fini(&tok);
}
xml_response(HTTP_OK, txn, root->doc);
auth_freestate(authstate);
}
break;
default:
invalid_meth:
txn->error.desc = "Unsupported iTIP method";
txn->error.precond = ISCHED_INVALID_SCHED;
ret = HTTP_BAD_REQUEST;
goto done;
}
break;
default:
txn->error.desc = "Unsupported iCalendar component";
txn->error.precond = ISCHED_INVALID_SCHED;
ret = HTTP_BAD_REQUEST;
}
done:
if (ical) icalcomponent_free(ical);
return ret;
}
int isched_send(struct caldav_sched_param *sparam, const char *recipient,
icalcomponent *ical, xmlNodePtr *xml)
{
int r = 0;
struct backend *be;
static unsigned send_count = 0;
static struct buf hdrs = BUF_INITIALIZER;
const char *body, *uri, *originator;
size_t bodylen;
icalproperty_method method;
icalcomponent *comp;
icalcomponent_kind kind;
icalproperty *prop;
unsigned code;
struct transaction_t txn;
struct http_connection conn;
*xml = NULL;
memset(&txn, 0, sizeof(struct transaction_t));
if (sparam->flags & SCHEDTYPE_REMOTE) uri = ISCHED_WELLKNOWN_URI;
else uri = namespace_ischedule.prefix;
/* Open connection to iSchedule receiver.
Use header buffer to construct remote server[:port][/tls][/noauth] */
buf_setcstr(&txn.buf, sparam->server);
if (sparam->port) buf_printf(&txn.buf, ":%u", sparam->port);
if (sparam->flags & SCHEDTYPE_SSL) buf_appendcstr(&txn.buf, "/tls");
if (sparam->flags & SCHEDTYPE_REMOTE) {
/* Using DKIM rather than HTTP Auth */
buf_appendcstr(&txn.buf, "/noauth");
}
be = proxy_findserver(buf_cstring(&txn.buf), &http_protocol, httpd_userid,
&backend_cached, NULL, NULL, httpd_in);
if (!be) return HTTP_UNAVAILABLE;
/* Setup HTTP connection for reading response */
memset(&conn, 0, sizeof(struct http_connection));
conn.pin = be->in;
txn.conn = &conn;
/* Create iSchedule request body */
body = icalcomponent_as_ical_string(ical);
bodylen = strlen(body);
/* Create iSchedule request header.
* XXX Make sure that we don't use multiple headers of the same name
* or add WSP around commas in signed headers
* to obey ischedule-relaxed canonicalization.
*/
buf_reset(&hdrs);
buf_printf(&hdrs, "Host: %s", sparam->server);
if (sparam->port) buf_printf(&hdrs, ":%u", sparam->port);
buf_printf(&hdrs, "\r\n");
buf_printf(&hdrs, "Cache-Control: no-cache, no-transform\r\n");
if (config_serverinfo == IMAP_ENUM_SERVERINFO_ON) {
buf_printf(&hdrs, "User-Agent: %s\r\n", buf_cstring(&serverinfo));
}
buf_printf(&hdrs, "iSchedule-Version: 1.0\r\n");
buf_printf(&hdrs, "iSchedule-Message-ID: <cmu-ischedule-%u-%ld-%u@%s>\r\n",
getpid(), time(NULL), send_count++, config_servername);
buf_printf(&hdrs, "Content-Type: text/calendar; charset=utf-8");
method = icalcomponent_get_method(ical);
comp = icalcomponent_get_first_real_component(ical);
kind = icalcomponent_isa(comp);
buf_printf(&hdrs, "; method=%s; component=%s\r\n",
icalproperty_method_to_string(method),
icalcomponent_kind_to_string(kind));
buf_printf(&hdrs, "Content-Length: %u\r\n", (unsigned) bodylen);
/* Determine Originator based on method and component */
if (method == ICAL_METHOD_REPLY) {
prop = icalcomponent_get_first_invitee(comp);
originator = icalproperty_get_invitee(prop);
}
else {
prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
originator = icalproperty_get_organizer(prop);
}
buf_printf(&hdrs, "Originator: %s\r\n", originator);
if (recipient) {
/* Single recipient */
buf_printf(&hdrs, "Recipient: %s\r\n", recipient);
}
else {
/* VFREEBUSY REQUEST - use ATTENDEES as Recipients */
char sep = ' ';
buf_printf(&hdrs, "Recipient:");
for (prop = icalcomponent_get_first_property(comp,
ICAL_ATTENDEE_PROPERTY);
prop;
prop = icalcomponent_get_next_property(comp,
ICAL_ATTENDEE_PROPERTY)) {
buf_printf(&hdrs, "%c%s", sep, icalproperty_get_attendee(prop));
sep = ',';
}
buf_printf(&hdrs, "\r\n");
}
/* Don't send body until told to */
buf_appendcstr(&hdrs, "Expect: 100-continue\r\n");
if (sparam->flags & SCHEDTYPE_REMOTE) {
#ifdef WITH_DKIM
DKIM *dkim = NULL;
DKIM_STAT stat;
unsigned char *sig = NULL;
size_t siglen;
const char *selector = config_getstring(IMAPOPT_ISCHEDULE_DKIM_SELECTOR);
const char *domain = config_getstring(IMAPOPT_ISCHEDULE_DKIM_DOMAIN);
/* Create iSchedule/DKIM signature */
if (dkim_lib &&
(dkim = dkim_sign(dkim_lib, NULL /* id */, NULL,
(dkim_sigkey_t) buf_cstring(&privkey),
(const u_char *) selector,
(const u_char *) domain,
/* Requires modified version of OpenDKIM
until we get OpenDOSETA */
DKIM_CANON_ISCHEDULE, DKIM_CANON_SIMPLE,
DKIM_SIGN_RSASHA256, -1 /* entire body */,
&stat))) {
/* Suppress folding of DKIM header */
// stat = dkim_set_margin(dkim, 0);
/* Add our query method list */
stat = dkim_add_querymethod(dkim, "private-exchange", NULL);
stat = dkim_add_querymethod(dkim, "http", "well-known");
// stat = dkim_add_querymethod(dkim, "dns", "txt");
/* Process the headers and body */
stat = dkim_chunk(dkim,
(u_char *) buf_cstring(&hdrs), buf_len(&hdrs));
stat = dkim_chunk(dkim, (u_char *) "\r\n", 2);
stat = dkim_chunk(dkim, (u_char *) body, bodylen);
stat = dkim_chunk(dkim, NULL, 0);
stat = dkim_eom(dkim, NULL);
/* Generate the signature */
stat = dkim_getsighdr_d(dkim, strlen(DKIM_SIGNHEADER) + 2,
&sig, &siglen);
/* Append a DKIM-Signature header */
buf_printf(&hdrs, "%s: %s\r\n", DKIM_SIGNHEADER, sig);
dkim_free(dkim);
}
#else
syslog(LOG_WARNING, "DKIM-Signature required, but DKIM isn't supported");
#endif /* WITH_DKIM */
}
redirect:
/* Send request line */
prot_printf(be->out, "POST %s %s\r\n", uri, HTTP_VERSION);
/* Send request headers */
prot_putbuf(be->out, &hdrs);
prot_puts(be->out, "\r\n");
response:
/* Read response (req_hdr and req_body are actually the response) */
txn.req_body.flags = BODY_DECODE;
r = http_read_response(be, METH_POST, &code,
&txn.req_hdrs, &txn.req_body, &txn.error.desc);
syslog(LOG_INFO, "isched_send(%s, %s) => %u",
recipient, buf_cstring(&txn.buf), code);
if (!r) {
switch (code) {
case 100: /* Continue */
/* Send request body (only ONCE for a given request) */
prot_write(be->out, body, bodylen);
bodylen = 0;
GCC_FALLTHROUGH
case 102:
case 103: /* Provisional */
goto response;
case 200: /* Successful */
/* Create XML parser context */
if (!(conn.xml = xmlNewParserCtxt())) {
fatal("Unable to create XML parser", EX_TEMPFAIL);
}
r = parse_xml_body(&txn, xml, NULL);
xmlFreeParserCtxt(conn.xml);
break;
case 301:
case 302:
case 307:
case 308: /* Redirection */
uri = spool_getheader(txn.req_hdrs, "Location")[0];
if (txn.req_body.flags & BODY_CLOSE) {
proxy_downserver(be);
be = proxy_findserver(buf_cstring(&txn.buf), &http_protocol,
NULL, &backend_cached,
NULL, NULL, httpd_in);
if (!be) {
r = HTTP_UNAVAILABLE;
break;
}
}
if (!bodylen) bodylen = strlen(body); /* Need to send body again */
goto redirect;
default:
r = HTTP_UNAVAILABLE;
}
}
if (txn.req_hdrs) spool_free_hdrcache(txn.req_hdrs);
buf_free(&txn.req_body.payload);
buf_free(&txn.buf);
return r;
}
#ifdef WITH_DKIM
static DKIM_CBSTAT isched_get_key(DKIM *dkim, DKIM_SIGINFO *sig,
u_char *buf, size_t buflen)
{
DKIM_CBSTAT stat = DKIM_CBSTAT_NOTFOUND;
const char *domain, *selector, *query;
tok_t tok;
char *type, *opts;
assert(dkim != NULL);
assert(sig != NULL);
domain = (const char *) dkim_sig_getdomain(sig);
selector = (const char *) dkim_sig_getselector(sig);
if (!domain || !selector) return DKIM_CBSTAT_ERROR;
query = (const char *) dkim_sig_gettagvalue(sig, 0, (u_char *) "q");
if (!query) query = "dns/txt"; /* implicit default */
/* Parse the q= tag */
tok_init(&tok, query, ":", 0);
while ((type = tok_next(&tok))) {
/* Split type/options */
if ((opts = strchr(type, '/'))) *opts++ = '\0';
#ifdef IOPTEST /* CalConnect ioptest */
if (1) {
#else
if (!strcmp(type, "private-exchange")) {
#endif
const char *prefix = config_getstring(IMAPOPT_HTTPDOCROOT);
struct buf path = BUF_INITIALIZER;
FILE *f;
if (!prefix) continue;
buf_setcstr(&path, prefix);
buf_printf(&path, "%s/%s/%s",
namespace_domainkey.prefix, domain, selector);
syslog(LOG_DEBUG, "using public key from path: %s",
buf_cstring(&path));
if (!(f = fopen(buf_cstring(&path), "r"))) {
syslog(LOG_NOTICE, "%s: fopen(): %s",
buf_cstring(&path), strerror(errno));
}
buf_free(&path);
if (!f) continue;
memset(buf, '\0', buflen);
fgets((char *) buf, buflen, f);
fclose(f);
if (buf[0] != '\0') {
stat = DKIM_CBSTAT_CONTINUE;
break;
}
}
else if (!strcmp(type, "http") && !strcmp(opts, "well-known")) {
}
else if (!strcmp(type, "dns") && !strcmp(opts, "txt")) {
stat = DKIM_CBSTAT_DEFAULT;
break;
}
}
tok_fini(&tok);
return stat;
}
static void dkim_cachehdr(const char *name, const char *contents,
const char *raw __attribute__((unused)), void *rock)
{
struct buf *hdrfield = &tmpbuf;
static const char *lastname = NULL;
int dup_hdr = name && lastname && !strcmp(name, lastname);
/* Ignore private headers in our cache */
if (name && name[0] == ':') return;
/* Combine header fields of the same name.
* Our hash table will always feed us duplicate headers consecutively.
*/
if (lastname && !dup_hdr) {
dkim_header((DKIM *) rock,
(u_char *) buf_cstring(hdrfield), buf_len(hdrfield));
}
lastname = name;
if (name) {
tok_t tok;
char *token, sep = ':';
if (!dup_hdr) buf_setcstr(hdrfield, name);
else sep = ',';
/* Trim leading/trailing WSP around comma-separated values */
tok_init(&tok, contents, ",", TOK_TRIMLEFT|TOK_TRIMRIGHT|TOK_EMPTY);
while ((token = tok_next(&tok))) {
buf_printf(hdrfield, "%c%s", sep, token);
sep = ',';
}
tok_fini(&tok);
}
}
static int dkim_auth(struct transaction_t *txn)
{
int authd = 0;
DKIM *dkim = NULL;
DKIM_STAT stat;
if (!dkim_lib) return 0;
dkim = dkim_verify(dkim_lib, NULL /* id */, NULL, &stat);
if (!dkim) return 0;
#ifdef TEST
{
/* XXX Hack for local testing */
dkim_query_t qtype = DKIM_QUERY_FILE;
struct buf keyfile = BUF_INITIALIZER;
stat = dkim_options(dkim_lib, DKIM_OP_SETOPT, DKIM_OPTS_QUERYMETHOD,
&qtype, sizeof(qtype));
buf_printf(&keyfile, "%s/dkim.public", config_dir);
stat = dkim_options(dkim_lib, DKIM_OP_SETOPT, DKIM_OPTS_QUERYINFO,
(void *) buf_cstring(&keyfile),
buf_len(&keyfile));
}
#endif
/* Process the cached headers and body */
spool_enum_hdrcache(txn->req_hdrs, &dkim_cachehdr, dkim);
dkim_cachehdr(NULL, NULL, dkim); /* Force canon of last header */
stat = dkim_eoh(dkim);
if (stat == DKIM_STAT_OK) {
stat = dkim_body(dkim, (u_char *) buf_cstring(&txn->req_body.payload),
buf_len(&txn->req_body.payload));
stat = dkim_eom(dkim, NULL);
}
if (stat == DKIM_STAT_OK) authd = 1;
else if (stat == DKIM_STAT_CBREJECT) {
txn->error.desc =
"Unable to verify: HTTP request-line mismatch";
}
else {
DKIM_SIGINFO *sig = dkim_getsignature(dkim);
if (sig) {
const char *sigerr, *sslerr;
if (dkim_sig_getbh(sig) == DKIM_SIGBH_MISMATCH)
sigerr = "body hash mismatch";
else {
DKIM_SIGERROR err = dkim_sig_geterror(sig);
sigerr = dkim_sig_geterrorstr(err);
}
assert(!buf_len(&txn->buf));
buf_printf(&txn->buf, "%s: %s",
dkim_getresultstr(stat), sigerr);
if ((sslerr = dkim_sig_getsslbuf(sig)))
buf_printf(&txn->buf, ": %s", sslerr);
txn->error.desc = buf_cstring(&txn->buf);
}
else txn->error.desc = dkim_getresultstr(stat);
}
dkim_free(dkim);
return authd;
}
#else
static int dkim_auth(struct transaction_t *txn __attribute__((unused)))
{
if (!config_getswitch(IMAPOPT_ISCHEDULE_DKIM_REQUIRED)) return 1;
syslog(LOG_WARNING, "DKIM-Signature provided, but DKIM isn't supported");
return 0;
}
#endif /* WITH_DKIM */
/* Perform a GET/HEAD request for a domainkey */
static int meth_get_domainkey(struct transaction_t *txn,
void *params __attribute__((unused)))
{
int ret = 0, r, fd = -1, precond;
const char *path;
static struct buf pathbuf = BUF_INITIALIZER;
struct stat sbuf;
const char *msg_base = NULL;
size_t msg_size = 0;
struct resp_body_t *resp_body = &txn->resp_body;
/* See if file exists and get Content-Length & Last-Modified time */
buf_setcstr(&pathbuf, config_dir);
buf_appendcstr(&pathbuf, txn->req_uri->path);
path = buf_cstring(&pathbuf);
r = stat(path, &sbuf);
if (r || !S_ISREG(sbuf.st_mode)) return HTTP_NOT_FOUND;
/* Generate Etag */
assert(!buf_len(&txn->buf));
buf_printf(&txn->buf, "%ld-%ld", (long) sbuf.st_mtime, (long) sbuf.st_size);
/* Check any preconditions, including range request */
txn->flags.ranges = 1;
precond = check_precond(txn, buf_cstring(&txn->buf), sbuf.st_mtime);
switch (precond) {
case HTTP_OK:
case HTTP_PARTIAL:
case HTTP_NOT_MODIFIED:
/* Fill in Content-Type, ETag, Last-Modified, and Expires */
resp_body->type = "text/plain";
resp_body->etag = buf_cstring(&txn->buf);
resp_body->lastmod = sbuf.st_mtime;
resp_body->maxage = 86400; /* 24 hrs */
txn->flags.cc |= CC_MAXAGE | CC_REVALIDATE;
if (!httpd_userisanonymous) txn->flags.cc |= CC_PUBLIC;
if (precond != HTTP_NOT_MODIFIED) break;
GCC_FALLTHROUGH
default:
/* We failed a precondition - don't perform the request */
resp_body->type = NULL;
return precond;
}
if (txn->meth == METH_GET) {
/* Open and mmap the file */
if ((fd = open(path, O_RDONLY)) == -1) return HTTP_SERVER_ERROR;
map_refresh(fd, 1, &msg_base, &msg_size, sbuf.st_size, path, NULL);
}
write_body(precond, txn, msg_base, sbuf.st_size);
if (fd != -1) {
map_free(&msg_base, &msg_size);
close(fd);
}
return ret;
}
static void isched_init(struct buf *serverinfo)
{
if (!(config_httpmodules & IMAP_ENUM_HTTPMODULES_CALDAV) ||
!config_getenum(IMAPOPT_CALDAV_ALLOWSCHEDULING)) {
/* Need CALDAV and CALDAV_SCHED in order to have ISCHEDULE */
return;
}
compile_time = calc_compile_time(__TIME__, __DATE__);
if (config_mupdate_server && config_getstring(IMAPOPT_PROXYSERVERS)) {
/* If backend server, we require iSchedule (w/o DKIM) */
namespace_ischedule.enabled = -1;
buf_cstring(serverinfo); // squash compiler warning when #undef WITH_DKIM
}
else {
namespace_ischedule.enabled =
config_httpmodules & IMAP_ENUM_HTTPMODULES_ISCHEDULE;
}
#ifdef WITH_DKIM
/* Add OpenDKIM version to serverinfo string */
uint32_t ver = dkim_libversion();
buf_printf(serverinfo, " OpenDKIM/%u.%u.%u",
(ver >> 24) & 0xff, (ver >> 16) & 0xff, (ver >> 8) & 0xff);
if (ver & 0xff) buf_printf(serverinfo, ".%u", ver & 0xff);
if (namespace_ischedule.enabled) {
int fd;
unsigned flags = ( DKIM_LIBFLAGS_BADSIGHANDLES | DKIM_LIBFLAGS_CACHE |
// DKIM_LIBFLAGS_KEEPFILES | DKIM_LIBFLAGS_TMPFILES |
DKIM_LIBFLAGS_VERIFYONE );
uint64_t ttl = 3600; /* 1 hour */
const char *requiredhdrs[] = { "Content-Type", "iSchedule-Version",
"Originator", "Recipient", NULL };
const char *signhdrs[] = { "iSchedule-Message-ID", "User-Agent", NULL };
const char *skiphdrs[] = { "Cache-Control", "Connection",
"Content-Length", "Host", "Keep-Alive",
"Proxy-Authenticate", "Proxy-Authorization",
"TE", "Trailer", "Transfer-Encoding",
"Upgrade", "Via", NULL };
const char *senderhdrs[] = { "Originator", NULL };
const char *keyfile = config_getstring(IMAPOPT_ISCHEDULE_DKIM_KEY_FILE);
unsigned need_dkim =
namespace_ischedule.enabled == IMAP_ENUM_HTTPMODULES_ISCHEDULE;
/* Initialize DKIM library */
if (!(dkim_lib = dkim_init(NULL, NULL))) {
syslog(LOG_ERR, "unable to initialize libopendkim");
namespace_ischedule.enabled = !need_dkim;
return;
}
/* Install our callback for doing key lookups */
dkim_set_key_lookup(dkim_lib, isched_get_key);
/* Setup iSchedule DKIM options */
#ifdef TEST
flags |= ( DKIM_LIBFLAGS_SIGNLEN | DKIM_LIBFLAGS_ZTAGS );
#endif
dkim_options(dkim_lib, DKIM_OP_SETOPT, DKIM_OPTS_FLAGS,
&flags, sizeof(flags));
dkim_options(dkim_lib, DKIM_OP_SETOPT, DKIM_OPTS_SIGNATURETTL,
&ttl, sizeof(ttl));
dkim_options(dkim_lib, DKIM_OP_SETOPT, DKIM_OPTS_REQUIREDHDRS,
requiredhdrs, sizeof(const char **));
dkim_options(dkim_lib, DKIM_OP_SETOPT, DKIM_OPTS_MUSTBESIGNED,
requiredhdrs, sizeof(const char **));
dkim_options(dkim_lib, DKIM_OP_SETOPT, DKIM_OPTS_SIGNHDRS,
signhdrs, sizeof(const char **));
dkim_options(dkim_lib, DKIM_OP_SETOPT, DKIM_OPTS_SKIPHDRS,
skiphdrs, sizeof(const char **));
dkim_options(dkim_lib, DKIM_OP_SETOPT, DKIM_OPTS_SENDERHDRS,
senderhdrs, sizeof(const char **));
/* Fetch DKIM private key for signing */
if (keyfile && (fd = open(keyfile, O_RDONLY)) != -1) {
const char *base = NULL;
size_t len = 0;
map_refresh(fd, 1, &base, &len, MAP_UNKNOWN_LEN, keyfile, NULL);
buf_setmap(&privkey, base, len);
map_free(&base, &len);
close(fd);
}
else {
syslog(LOG_ERR, "unable to open private key file %s", keyfile);
namespace_ischedule.enabled = !need_dkim;
}
namespace_domainkey.enabled =
config_httpmodules & IMAP_ENUM_HTTPMODULES_DOMAINKEY;
}
#endif /* WITH_DKIM */
}
static void isched_shutdown(void)
{
#ifdef WITH_DKIM
buf_free(&privkey);
buf_free(&tmpbuf);
buf_free(&b64req);
if (dkim_lib) dkim_close(dkim_lib);
#endif
}