Page MenuHomePhorge

httpd.c
No OneTemporary

Authored By
Unknown
Size
101 KB
Referenced Files
None
Subscribers
None
/* httpd.c -- HTTP/WebDAV/CalDAV server protocol parsing
*
* Copyright (c) 1994-2011 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 <stdio.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/param.h>
#include <syslog.h>
#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <ctype.h>
#include "prot.h"
#include <sasl/sasl.h>
#include <sasl/saslutil.h>
#include "httpd.h"
#include "http_proxy.h"
#include "acl.h"
#include "assert.h"
#include "util.h"
#include "iptostring.h"
#include "global.h"
#include "tls.h"
#include "map.h"
#include "acl.h"
#include "exitcodes.h"
#include "imapd.h"
#include "imap_err.h"
#include "http_err.h"
#include "proc.h"
#include "version.h"
#include "xstrlcpy.h"
#include "xstrlcat.h"
#include "sync_log.h"
#include "telemetry.h"
#include "backend.h"
#include "proxy.h"
#include "userdeny.h"
#include "message.h"
#include "idle.h"
#include "times.h"
#include "tok.h"
#include "wildmat.h"
#include "md5.h"
#ifdef WITH_DAV
#include "http_dav.h"
#endif
#include <libxml/tree.h>
#include <libxml/HTMLtree.h>
#include <libxml/uri.h>
#ifdef HAVE_ZLIB
#include <zlib.h>
#endif /* HAVE_ZLIB */
static const char tls_message[] =
HTML_DOCTYPE
"<html>\n<head>\n<title>TLS Required</title>\n</head>\n" \
"<body>\n<h2>TLS is required to use Basic authentication</h2>\n" \
"Use <a href=\"%s\">%s</a> instead.\n" \
"</body>\n</html>\n";
extern int optind;
extern char *optarg;
extern int opterr;
#ifdef HAVE_SSL
static SSL *tls_conn;
#endif /* HAVE_SSL */
sasl_conn_t *httpd_saslconn; /* the sasl connection context */
static struct wildmat *allow_cors = NULL;
int httpd_timeout, httpd_keepalive;
char *httpd_userid = NULL, *proxy_userid = NULL;
char *httpd_extradomain = NULL;
struct auth_state *httpd_authstate = 0;
int httpd_userisadmin = 0;
int httpd_userisproxyadmin = 0;
int httpd_userisanonymous = 1;
struct sockaddr_storage httpd_localaddr, httpd_remoteaddr;
int httpd_haveaddr = 0;
char httpd_clienthost[NI_MAXHOST*2+1] = "[local]";
struct protstream *httpd_out = NULL;
struct protstream *httpd_in = NULL;
struct protgroup *protin = NULL;
static int httpd_logfd = -1;
static sasl_ssf_t extprops_ssf = 0;
int https = 0;
int httpd_tls_done = 0;
int httpd_tls_required = 0;
unsigned avail_auth_schemes = 0; /* bitmask of available auth schemes */
unsigned long config_httpmodules;
int config_httpprettytelemetry;
static time_t compile_time;
struct buf serverinfo = BUF_INITIALIZER;
static void digest_send_success(const char *name __attribute__((unused)),
const char *data)
{
prot_printf(httpd_out, "Authentication-Info: %s\r\n", data);
}
/* List of HTTP auth schemes that we support */
struct auth_scheme_t auth_schemes[] = {
{ AUTH_BASIC, "Basic", NULL, AUTH_SERVER_FIRST | AUTH_BASE64, NULL, NULL },
{ AUTH_DIGEST, "Digest", HTTP_DIGEST_MECH, AUTH_NEED_REQUEST|AUTH_SERVER_FIRST,
&digest_send_success, digest_recv_success },
{ AUTH_SPNEGO, "Negotiate", "GSS-SPNEGO", AUTH_BASE64, NULL, NULL },
{ AUTH_NTLM, "NTLM", "NTLM", AUTH_NEED_PERSIST | AUTH_BASE64, NULL, NULL },
{ -1, NULL, NULL, 0, NULL, NULL }
};
/* the sasl proxy policy context */
static struct proxy_context httpd_proxyctx = {
0, 1, &httpd_authstate, &httpd_userisadmin, &httpd_userisproxyadmin
};
/* signal to config.c */
const int config_need_data = CONFIG_NEED_PARTITION_DATA;
/* current namespace */
HIDDEN struct namespace httpd_namespace;
/* PROXY STUFF */
/* we want a list of our outgoing connections here and which one we're
currently piping */
/* the current server most commands go to */
struct backend *backend_current = NULL;
/* our cached connections */
struct backend **backend_cached = NULL;
/* end PROXY stuff */
static void starttls(int https);
void usage(void);
void shut_down(int code) __attribute__ ((noreturn));
/* Enable the resetting of a sasl_conn_t */
static int reset_saslconn(sasl_conn_t **conn);
static void cmdloop(void);
static int parse_expect(struct transaction_t *txn);
static void parse_connection(struct transaction_t *txn);
static int parse_ranges(const char *hdr, unsigned long len,
struct range **ranges);
static int proxy_authz(const char **authzid, struct transaction_t *txn);
static void auth_success(struct transaction_t *txn);
static int http_auth(const char *creds, struct transaction_t *txn);
static void keep_alive(int sig);
static int meth_get(struct transaction_t *txn, void *params);
static int meth_propfind_root(struct transaction_t *txn, void *params);
static struct {
char *ipremoteport;
char *iplocalport;
sasl_ssf_t ssf;
char *authid;
} saslprops = {NULL,NULL,0,NULL};
static struct sasl_callback mysasl_cb[] = {
{ SASL_CB_GETOPT, (mysasl_cb_ft *) &mysasl_config, NULL },
{ SASL_CB_PROXY_POLICY, (mysasl_cb_ft *) &mysasl_proxy_policy, (void*) &httpd_proxyctx },
{ SASL_CB_CANON_USER, (mysasl_cb_ft *) &mysasl_canon_user, NULL },
{ SASL_CB_LIST_END, NULL, NULL }
};
/* Array of HTTP methods known by our server. */
const struct known_meth_t http_methods[] = {
{ "ACL", 0 },
{ "COPY", METH_NOBODY },
{ "DELETE", METH_NOBODY },
{ "GET", METH_NOBODY },
{ "HEAD", METH_NOBODY },
{ "LOCK", 0 },
{ "MKCALENDAR", 0 },
{ "MKCOL", 0 },
{ "MOVE", METH_NOBODY },
{ "OPTIONS", METH_NOBODY },
{ "POST", 0 },
{ "PROPFIND", 0 },
{ "PROPPATCH", 0 },
{ "PUT", 0 },
{ "REPORT", 0 },
{ "TRACE", METH_NOBODY },
{ "UNLOCK", METH_NOBODY },
{ NULL, 0 }
};
/* Namespace to fetch static content from filesystem */
struct namespace_t namespace_default = {
URL_NS_DEFAULT, 1, "", NULL, 0 /* no auth */,
/*mbtype*/0,
ALLOW_READ,
NULL, NULL, NULL, NULL,
{
{ NULL, NULL }, /* ACL */
{ NULL, NULL }, /* COPY */
{ NULL, NULL }, /* DELETE */
{ &meth_get, NULL }, /* GET */
{ &meth_get, NULL }, /* HEAD */
{ NULL, NULL }, /* LOCK */
{ NULL, NULL }, /* MKCALENDAR */
{ NULL, NULL }, /* MKCOL */
{ NULL, NULL }, /* MOVE */
{ &meth_options, NULL }, /* OPTIONS */
{ NULL, NULL }, /* POST */
{ &meth_propfind_root, NULL }, /* PROPFIND */
{ NULL, NULL }, /* PROPPATCH */
{ NULL, NULL }, /* PUT */
{ NULL, NULL }, /* REPORT */
{ &meth_trace, NULL }, /* TRACE */
{ NULL, NULL }, /* UNLOCK */
}
};
/* Array of different namespaces and features supported by the server */
struct namespace_t *namespaces[] = {
#ifdef WITH_DAV
#ifdef WITH_JSON
&namespace_tzdist, /* MUST be before namespace_calendar!! */
#endif /* WITH_JSON */
&namespace_principal,
&namespace_calendar,
&namespace_freebusy,
&namespace_addressbook,
#ifdef HAVE_IANA_PARAMS
&namespace_ischedule,
&namespace_domainkey,
#endif /* HAVE_IANA_PARAMS */
#endif /* WITH_DAV */
#ifdef WITH_JSON
&namespace_jmap,
#endif /* WITH_JSON */
&namespace_rss,
&namespace_dblookup,
&namespace_default, /* MUST be present and be last!! */
NULL,
};
static void httpd_reset(void)
{
int i;
int bytes_in = 0;
int bytes_out = 0;
/* Do any namespace specific cleanup */
for (i = 0; namespaces[i]; i++) {
if (namespaces[i]->enabled && namespaces[i]->reset)
namespaces[i]->reset();
}
proc_cleanup();
/* close backend connections */
i = 0;
while (backend_cached && backend_cached[i]) {
proxy_downserver(backend_cached[i]);
free(backend_cached[i]->context);
free(backend_cached[i]);
i++;
}
if (backend_cached) free(backend_cached);
backend_cached = NULL;
backend_current = NULL;
if (httpd_in) {
prot_NONBLOCK(httpd_in);
prot_fill(httpd_in);
bytes_in = prot_bytes_in(httpd_in);
prot_free(httpd_in);
}
if (httpd_out) {
prot_flush(httpd_out);
bytes_out = prot_bytes_out(httpd_out);
prot_free(httpd_out);
}
if (config_auditlog) {
syslog(LOG_NOTICE,
"auditlog: traffic sessionid=<%s> bytes_in=<%d> bytes_out=<%d>",
session_id(), bytes_in, bytes_out);
}
httpd_in = httpd_out = NULL;
if (protin) protgroup_reset(protin);
#ifdef HAVE_SSL
if (tls_conn) {
tls_reset_servertls(&tls_conn);
tls_conn = NULL;
}
#endif
cyrus_reset_stdio();
strcpy(httpd_clienthost, "[local]");
if (httpd_logfd != -1) {
close(httpd_logfd);
httpd_logfd = -1;
}
if (httpd_userid != NULL) {
free(httpd_userid);
httpd_userid = NULL;
}
httpd_userisanonymous = 1;
if (httpd_extradomain != NULL) {
free(httpd_extradomain);
httpd_extradomain = NULL;
}
if (proxy_userid != NULL) {
free(proxy_userid);
proxy_userid = NULL;
}
if (httpd_authstate) {
auth_freestate(httpd_authstate);
httpd_authstate = NULL;
}
if (httpd_saslconn) {
sasl_dispose(&httpd_saslconn);
httpd_saslconn = NULL;
}
httpd_tls_done = 0;
if(saslprops.iplocalport) {
free(saslprops.iplocalport);
saslprops.iplocalport = NULL;
}
if(saslprops.ipremoteport) {
free(saslprops.ipremoteport);
saslprops.ipremoteport = NULL;
}
if(saslprops.authid) {
free(saslprops.authid);
saslprops.authid = NULL;
}
saslprops.ssf = 0;
}
/*
* run once when process is forked;
* MUST NOT exit directly; must return with non-zero error code
*/
int service_init(int argc __attribute__((unused)),
char **argv __attribute__((unused)),
char **envp __attribute__((unused)))
{
int r, opt, i, allow_trace = config_getswitch(IMAPOPT_HTTPALLOWTRACE);
LIBXML_TEST_VERSION
initialize_http_error_table();
if (geteuid() == 0) fatal("must run as the Cyrus user", EC_USAGE);
setproctitle_init(argc, argv, envp);
/* set signal handlers */
signals_set_shutdown(&shut_down);
signal(SIGPIPE, SIG_IGN);
/* load the SASL plugins */
global_sasl_init(1, 1, mysasl_cb);
/* open the mboxlist, we'll need it for real work */
mboxlist_init(0);
mboxlist_open(NULL);
/* open the quota db, we'll need it for expunge */
quotadb_init(0);
quotadb_open(NULL);
/* open the user deny db */
denydb_init(0);
denydb_open(/*create*/0);
/* open annotations.db, we'll need it for collection properties */
annotatemore_open();
/* setup for sending IMAP IDLE notifications */
idle_enabled();
/* Set namespace */
if ((r = mboxname_init_namespace(&httpd_namespace, 1)) != 0) {
syslog(LOG_ERR, "%s", error_message(r));
fatal(error_message(r), EC_CONFIG);
}
/* External names are in URIs (UNIX sep) */
httpd_namespace.hier_sep = '/';
/* open the mboxevent system */
mboxevent_init();
mboxevent_setnamespace(&httpd_namespace);
while ((opt = getopt(argc, argv, "sp:")) != EOF) {
switch(opt) {
case 's': /* https (do TLS right away) */
https = 1;
if (!tls_enabled()) {
syslog(LOG_ERR, "https: required OpenSSL options not present");
fatal("https: required OpenSSL options not present",
EC_CONFIG);
}
break;
case 'p': /* external protection */
extprops_ssf = atoi(optarg);
break;
default:
usage();
}
}
/* Create a protgroup for input from the client and selected backend */
protin = protgroup_new(2);
config_httpprettytelemetry = config_getswitch(IMAPOPT_HTTPPRETTYTELEMETRY);
if (config_getstring(IMAPOPT_HTTPALLOWCORS)) {
allow_cors =
split_wildmats((char *) config_getstring(IMAPOPT_HTTPALLOWCORS),
NULL);
}
/* Construct serverinfo string */
buf_printf(&serverinfo, "Cyrus-HTTP/%s Cyrus-SASL/%u.%u.%u",
cyrus_version(),
SASL_VERSION_MAJOR, SASL_VERSION_MINOR, SASL_VERSION_STEP);
#ifdef HAVE_SSL
buf_printf(&serverinfo, " OpenSSL/%s", SHLIB_VERSION_NUMBER);
#endif
#ifdef HAVE_ZLIB
buf_printf(&serverinfo, " Zlib/%s", ZLIB_VERSION);
#endif
buf_printf(&serverinfo, " LibXML%s", LIBXML_DOTTED_VERSION);
/* Do any namespace specific initialization */
config_httpmodules = config_getbitfield(IMAPOPT_HTTPMODULES);
for (i = 0; namespaces[i]; i++) {
if (allow_trace) namespaces[i]->allow |= ALLOW_TRACE;
if (namespaces[i]->init) namespaces[i]->init(&serverinfo);
}
compile_time = calc_compile_time(__TIME__, __DATE__);
return 0;
}
/*
* run for each accepted connection
*/
int service_main(int argc __attribute__((unused)),
char **argv __attribute__((unused)),
char **envp __attribute__((unused)))
{
socklen_t salen;
char hbuf[NI_MAXHOST];
char localip[60], remoteip[60];
int niflags;
sasl_security_properties_t *secprops=NULL;
const char *mechlist, *mech;
int mechcount = 0;
size_t mechlen;
struct auth_scheme_t *scheme;
session_new_id();
signals_poll();
sync_log_init();
httpd_in = prot_new(0, 0);
httpd_out = prot_new(1, 1);
protgroup_insert(protin, httpd_in);
/* Find out name of client host */
salen = sizeof(httpd_remoteaddr);
if (getpeername(0, (struct sockaddr *)&httpd_remoteaddr, &salen) == 0 &&
(httpd_remoteaddr.ss_family == AF_INET ||
httpd_remoteaddr.ss_family == AF_INET6)) {
if (getnameinfo((struct sockaddr *)&httpd_remoteaddr, salen,
hbuf, sizeof(hbuf), NULL, 0, NI_NAMEREQD) == 0) {
strncpy(httpd_clienthost, hbuf, sizeof(hbuf));
strlcat(httpd_clienthost, " ", sizeof(httpd_clienthost));
} else {
httpd_clienthost[0] = '\0';
}
niflags = NI_NUMERICHOST;
#ifdef NI_WITHSCOPEID
if (((struct sockaddr *)&httpd_remoteaddr)->sa_family == AF_INET6)
niflags |= NI_WITHSCOPEID;
#endif
if (getnameinfo((struct sockaddr *)&httpd_remoteaddr, salen, hbuf,
sizeof(hbuf), NULL, 0, niflags) != 0)
strlcpy(hbuf, "unknown", sizeof(hbuf));
strlcat(httpd_clienthost, "[", sizeof(httpd_clienthost));
strlcat(httpd_clienthost, hbuf, sizeof(httpd_clienthost));
strlcat(httpd_clienthost, "]", sizeof(httpd_clienthost));
salen = sizeof(httpd_localaddr);
if (getsockname(0, (struct sockaddr *)&httpd_localaddr, &salen) == 0) {
httpd_haveaddr = 1;
}
/* Create pre-authentication telemetry log based on client IP */
httpd_logfd = telemetry_log(hbuf, httpd_in, httpd_out, 0);
}
/* other params should be filled in */
if (sasl_server_new("HTTP", config_servername, NULL, NULL, NULL, NULL,
SASL_USAGE_FLAGS, &httpd_saslconn) != SASL_OK)
fatal("SASL failed initializing: sasl_server_new()",EC_TEMPFAIL);
/* will always return something valid */
secprops = mysasl_secprops(0);
/* no HTTP clients seem to use "auth-int" */
secprops->max_ssf = 0; /* "auth" only */
secprops->maxbufsize = 0; /* don't need maxbuf */
if (sasl_setprop(httpd_saslconn, SASL_SEC_PROPS, secprops) != SASL_OK)
fatal("Failed to set SASL property", EC_TEMPFAIL);
if (sasl_setprop(httpd_saslconn, SASL_SSF_EXTERNAL, &extprops_ssf) != SASL_OK)
fatal("Failed to set SASL property", EC_TEMPFAIL);
if(iptostring((struct sockaddr *)&httpd_localaddr,
salen, localip, 60) == 0) {
sasl_setprop(httpd_saslconn, SASL_IPLOCALPORT, localip);
saslprops.iplocalport = xstrdup(localip);
}
if(iptostring((struct sockaddr *)&httpd_remoteaddr,
salen, remoteip, 60) == 0) {
sasl_setprop(httpd_saslconn, SASL_IPREMOTEPORT, remoteip);
saslprops.ipremoteport = xstrdup(remoteip);
}
/* See which auth schemes are available to us */
if ((extprops_ssf >= 2) || config_getswitch(IMAPOPT_ALLOWPLAINTEXT)) {
avail_auth_schemes |= (1 << AUTH_BASIC);
}
sasl_listmech(httpd_saslconn, NULL, NULL, " ", NULL,
&mechlist, NULL, &mechcount);
for (mech = mechlist; mechcount--; mech += ++mechlen) {
mechlen = strcspn(mech, " \0");
for (scheme = auth_schemes; scheme->name; scheme++) {
if (scheme->saslmech && !strncmp(mech, scheme->saslmech, mechlen)) {
avail_auth_schemes |= (1 << scheme->idx);
break;
}
}
}
httpd_tls_required = !avail_auth_schemes;
proc_register("httpd", httpd_clienthost, NULL, NULL, NULL);
/* Set inactivity timer */
httpd_timeout = config_getint(IMAPOPT_HTTPTIMEOUT);
if (httpd_timeout < 0) httpd_timeout = 0;
httpd_timeout *= 60;
prot_settimeout(httpd_in, httpd_timeout);
prot_setflushonread(httpd_in, httpd_out);
/* we were connected on https port so we should do
TLS negotiation immediatly */
if (https == 1) starttls(1);
/* Setup the signal handler for keepalive heartbeat */
httpd_keepalive = config_getint(IMAPOPT_HTTPKEEPALIVE);
if (httpd_keepalive < 0) httpd_keepalive = 0;
if (httpd_keepalive) {
struct sigaction action;
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
#ifdef SA_RESTART
action.sa_flags |= SA_RESTART;
#endif
action.sa_handler = keep_alive;
if (sigaction(SIGALRM, &action, NULL) < 0) {
syslog(LOG_ERR, "unable to install signal handler for %d: %m", SIGALRM);
httpd_keepalive = 0;
}
}
cmdloop();
/* Closing connection */
/* cleanup */
signal(SIGALRM, SIG_IGN);
httpd_reset();
return 0;
}
/* Called by service API to shut down the service */
void service_abort(int error)
{
shut_down(error);
}
void usage(void)
{
prot_printf(httpd_out, "%s: usage: httpd [-C <alt_config>] [-s]\r\n",
error_message(HTTP_SERVER_ERROR));
prot_flush(httpd_out);
exit(EC_USAGE);
}
/*
* Cleanly shut down and exit
*/
void shut_down(int code)
{
int i;
int bytes_in = 0;
int bytes_out = 0;
in_shutdown = 1;
if (allow_cors) free_wildmats(allow_cors);
/* Do any namespace specific cleanup */
for (i = 0; namespaces[i]; i++) {
if (namespaces[i]->enabled && namespaces[i]->shutdown)
namespaces[i]->shutdown();
}
xmlCleanupParser();
proc_cleanup();
/* close backend connections */
i = 0;
while (backend_cached && backend_cached[i]) {
proxy_downserver(backend_cached[i]);
free(backend_cached[i]->context);
free(backend_cached[i]);
i++;
}
if (backend_cached) free(backend_cached);
sync_log_done();
mboxlist_close();
mboxlist_done();
quotadb_close();
quotadb_done();
denydb_close();
denydb_done();
annotatemore_close();
if (httpd_in) {
prot_NONBLOCK(httpd_in);
prot_fill(httpd_in);
bytes_in = prot_bytes_in(httpd_in);
prot_free(httpd_in);
}
if (httpd_out) {
prot_flush(httpd_out);
bytes_out = prot_bytes_out(httpd_out);
prot_free(httpd_out);
}
if (protin) protgroup_free(protin);
if (config_auditlog)
syslog(LOG_NOTICE,
"auditlog: traffic sessionid=<%s> bytes_in=<%d> bytes_out=<%d>",
session_id(), bytes_in, bytes_out);
#ifdef HAVE_SSL
tls_shutdown_serverengine();
#endif
cyrus_done();
exit(code);
}
void fatal(const char* s, int code)
{
static int recurse_code = 0;
if (recurse_code) {
/* We were called recursively. Just give up */
proc_cleanup();
exit(recurse_code);
}
recurse_code = code;
if (httpd_out) {
prot_printf(httpd_out,
"HTTP/1.1 %s\r\n"
"Content-Type: text/plain\r\n"
"Connection: close\r\n\r\n"
"Fatal error: %s\r\n",
error_message(HTTP_SERVER_ERROR), s);
prot_flush(httpd_out);
}
syslog(LOG_ERR, "Fatal error: %s", s);
shut_down(code);
}
#ifdef HAVE_SSL
static void starttls(int https)
{
int result;
int *layerp;
sasl_ssf_t ssf;
char *auth_id;
/* SASL and openssl have different ideas about whether ssf is signed */
layerp = (int *) &ssf;
result=tls_init_serverengine("http",
5, /* depth to verify */
!https); /* can client auth? */
if (result == -1) {
syslog(LOG_ERR, "[httpd] error initializing TLS");
fatal("tls_init() failed",EC_TEMPFAIL);
}
if (!https) {
/* tell client to start TLS upgrade (RFC 2817) */
response_header(HTTP_SWITCH_PROT, NULL);
}
result=tls_start_servertls(0, /* read */
1, /* write */
https ? 180 : httpd_timeout,
layerp,
&auth_id,
&tls_conn);
/* if error */
if (result == -1) {
syslog(LOG_NOTICE, "https failed: %s", httpd_clienthost);
fatal("tls_start_servertls() failed", EC_TEMPFAIL);
}
/* tell SASL about the negotiated layer */
result = sasl_setprop(httpd_saslconn, SASL_SSF_EXTERNAL, &ssf);
if (result != SASL_OK) {
fatal("sasl_setprop() failed: starttls()", EC_TEMPFAIL);
}
saslprops.ssf = ssf;
result = sasl_setprop(httpd_saslconn, SASL_AUTH_EXTERNAL, auth_id);
if (result != SASL_OK) {
fatal("sasl_setprop() failed: starttls()", EC_TEMPFAIL);
}
if (saslprops.authid) {
free(saslprops.authid);
saslprops.authid = NULL;
}
if (auth_id) saslprops.authid = xstrdup(auth_id);
/* tell the prot layer about our new layers */
prot_settls(httpd_in, tls_conn);
prot_settls(httpd_out, tls_conn);
httpd_tls_done = 1;
httpd_tls_required = 0;
avail_auth_schemes |= (1 << AUTH_BASIC);
}
#else
static void starttls(int https __attribute__((unused)))
{
fatal("starttls() called, but no OpenSSL", EC_SOFTWARE);
}
#endif /* HAVE_SSL */
/* Reset the given sasl_conn_t to a sane state */
static int reset_saslconn(sasl_conn_t **conn)
{
int ret;
sasl_security_properties_t *secprops = NULL;
sasl_dispose(conn);
/* do initialization typical of service_main */
ret = sasl_server_new("HTTP", config_servername, NULL, NULL, NULL, NULL,
SASL_USAGE_FLAGS, conn);
if(ret != SASL_OK) return ret;
if(saslprops.ipremoteport)
ret = sasl_setprop(*conn, SASL_IPREMOTEPORT,
saslprops.ipremoteport);
if(ret != SASL_OK) return ret;
if(saslprops.iplocalport)
ret = sasl_setprop(*conn, SASL_IPLOCALPORT,
saslprops.iplocalport);
if(ret != SASL_OK) return ret;
secprops = mysasl_secprops(0);
/* no HTTP clients seem to use "auth-int" */
secprops->max_ssf = 0; /* "auth" only */
secprops->maxbufsize = 0; /* don't need maxbuf */
ret = sasl_setprop(*conn, SASL_SEC_PROPS, secprops);
if(ret != SASL_OK) return ret;
/* end of service_main initialization excepting SSF */
/* If we have TLS/SSL info, set it */
if(saslprops.ssf) {
ret = sasl_setprop(*conn, SASL_SSF_EXTERNAL, &saslprops.ssf);
} else {
ret = sasl_setprop(*conn, SASL_SSF_EXTERNAL, &extprops_ssf);
}
if(ret != SASL_OK) return ret;
if(saslprops.authid) {
ret = sasl_setprop(*conn, SASL_AUTH_EXTERNAL, saslprops.authid);
if(ret != SASL_OK) return ret;
}
/* End TLS/SSL Info */
return SASL_OK;
}
/*
* Top-level command loop parsing
*/
static void cmdloop(void)
{
int gzip_enabled = 0;
struct transaction_t txn;
/* Start with an empty (clean) transaction */
memset(&txn, 0, sizeof(struct transaction_t));
/* Pre-allocate our working buffer */
buf_ensure(&txn.buf, 1024);
#ifdef HAVE_ZLIB
/* Always use gzip format because IE incorrectly uses raw deflate */
if (config_getswitch(IMAPOPT_HTTPALLOWCOMPRESS) &&
deflateInit2(&txn.zstrm, Z_DEFAULT_COMPRESSION, Z_DEFLATED,
16+MAX_WBITS, MAX_MEM_LEVEL, Z_DEFAULT_STRATEGY) == Z_OK) {
gzip_enabled = 1;
}
#endif
for (;;) {
int ret, empty, r, i, c;
char *p;
tok_t tok;
const char **hdr, *query;
const struct namespace_t *namespace;
const struct method_t *meth_t;
struct request_line_t *req_line = &txn.req_line;
/* Reset txn state */
txn.meth = METH_UNKNOWN;
memset(&txn.flags, 0, sizeof(struct txn_flags_t));
txn.flags.conn = 0;
txn.flags.vary = VARY_AE;
memset(req_line, 0, sizeof(struct request_line_t));
memset(&txn.req_tgt, 0, sizeof(struct request_target_t));
construct_hash_table(&txn.req_qparams, 10, 1);
txn.req_uri = NULL;
txn.auth_chal.param = NULL;
txn.req_hdrs = NULL;
txn.req_body.flags = 0;
buf_reset(&txn.req_body.payload);
txn.location = NULL;
memset(&txn.error, 0, sizeof(struct error_t));
memset(&txn.resp_body, 0, /* Don't zero the response payload buffer */
sizeof(struct resp_body_t) - sizeof(struct buf));
buf_reset(&txn.resp_body.payload);
buf_reset(&txn.buf);
ret = empty = 0;
/* Create header cache */
if (!(txn.req_hdrs = spool_new_hdrcache())) {
txn.error.desc = "Unable to create header cache";
ret = HTTP_SERVER_ERROR;
}
req_line:
do {
/* Flush any buffered output */
prot_flush(httpd_out);
if (backend_current) prot_flush(backend_current->out);
/* Check for shutdown file */
if (shutdown_file(txn.buf.s, txn.buf.alloc) ||
(httpd_userid &&
userdeny(httpd_userid, config_ident, txn.buf.s, txn.buf.alloc))) {
txn.error.desc = txn.buf.s;
ret = HTTP_UNAVAILABLE;
break;
}
signals_poll();
} while (!proxy_check_input(protin, httpd_in, httpd_out,
backend_current ? backend_current->in : NULL,
NULL, 0));
if (ret) {
txn.flags.conn = CONN_CLOSE;
error_response(ret, &txn);
protgroup_free(protin);
shut_down(0);
}
/* Read request-line */
syslog(LOG_DEBUG, "read & parse request-line");
if (!prot_fgets(req_line->buf, MAX_REQ_LINE+1, httpd_in)) {
txn.error.desc = prot_error(httpd_in);
if (txn.error.desc && strcmp(txn.error.desc, PROT_EOF_STRING)) {
/* client timed out */
syslog(LOG_WARNING, "%s, closing connection", txn.error.desc);
ret = HTTP_TIMEOUT;
}
else {
/* client closed connection */
}
txn.flags.conn = CONN_CLOSE;
goto done;
}
/* Trim CRLF from request-line */
p = req_line->buf + strlen(req_line->buf);
if (p[-1] == '\n') *--p = '\0';
if (p[-1] == '\r') *--p = '\0';
/* Ignore 1 empty line before request-line per RFC 7230 Sec 3.5 */
if (!empty++ && !*req_line->buf) goto req_line;
/* Parse request-line = method SP request-target SP HTTP-version CRLF */
tok_initm(&tok, req_line->buf, " ", 0);
if (!(req_line->meth = tok_next(&tok))) {
ret = HTTP_BAD_REQUEST;
txn.error.desc = "Missing method in request-line";
}
else if (!(req_line->uri = tok_next(&tok))) {
ret = HTTP_BAD_REQUEST;
txn.error.desc = "Missing request-target in request-line";
}
else if ((size_t) (p - req_line->buf) > MAX_REQ_LINE - 2) {
/* request-line overran the size of our buffer */
ret = HTTP_URI_TOO_LONG;
buf_printf(&txn.buf,
"Length of request-line MUST be less than %u octets",
MAX_REQ_LINE);
txn.error.desc = buf_cstring(&txn.buf);
}
else if (!(req_line->ver = tok_next(&tok))) {
ret = HTTP_BAD_REQUEST;
txn.error.desc = "Missing HTTP-version in request-line";
}
else if (tok_next(&tok)) {
ret = HTTP_BAD_REQUEST;
txn.error.desc = "Unexpected extra argument(s) in request-line";
}
/* Check HTTP-Version - MUST be HTTP/1.x */
else if (strlen(req_line->ver) != HTTP_VERSION_LEN
|| strncmp(req_line->ver, HTTP_VERSION, HTTP_VERSION_LEN-1)
|| !isdigit(req_line->ver[HTTP_VERSION_LEN-1])) {
ret = HTTP_BAD_VERSION;
buf_printf(&txn.buf,
"This server only speaks %.*sx",
HTTP_VERSION_LEN-1, HTTP_VERSION);
txn.error.desc = buf_cstring(&txn.buf);
}
else if (req_line->ver[HTTP_VERSION_LEN-1] == '0') {
/* HTTP/1.0 connection */
txn.flags.ver1_0 = 1;
}
tok_fini(&tok);
if (ret) {
txn.flags.conn = CONN_CLOSE;
goto done;
}
/* Read and parse headers */
syslog(LOG_DEBUG, "read & parse headers");
if ((r = spool_fill_hdrcache(httpd_in, NULL, txn.req_hdrs, NULL))) {
ret = HTTP_BAD_REQUEST;
txn.error.desc = error_message(r);
}
else if ((txn.error.desc = prot_error(httpd_in)) &&
strcmp(txn.error.desc, PROT_EOF_STRING)) {
/* client timed out */
syslog(LOG_WARNING, "%s, closing connection", txn.error.desc);
ret = HTTP_TIMEOUT;
}
/* Read CRLF separating headers and body */
else if ((c = prot_getc(httpd_in)) != '\r' ||
(c = prot_getc(httpd_in)) != '\n') {
ret = HTTP_BAD_REQUEST;
txn.error.desc = error_message(IMAP_MESSAGE_NOBLANKLINE);
}
if (ret) {
txn.flags.conn = CONN_CLOSE;
goto done;
}
/* Check for Connection options */
parse_connection(&txn);
if (txn.flags.conn & CONN_UPGRADE) {
starttls(0);
txn.flags.conn &= ~CONN_UPGRADE;
}
/* Check for HTTP method override */
if (!strcmp(req_line->meth, "POST") &&
(hdr = spool_getheader(txn.req_hdrs, "X-HTTP-Method-Override"))) {
txn.flags.override = 1;
req_line->meth = (char *) hdr[0];
}
/* Check Method against our list of known methods */
for (txn.meth = 0; (txn.meth < METH_UNKNOWN) &&
strcmp(http_methods[txn.meth].name, req_line->meth);
txn.meth++);
if (txn.meth == METH_UNKNOWN) ret = HTTP_NOT_IMPLEMENTED;
/* Parse request-target URI */
else if (!(txn.req_uri = parse_uri(txn.meth, req_line->uri, 1,
&txn.error.desc))) {
ret = HTTP_BAD_REQUEST;
}
/* Check message framing */
else if ((r = http_parse_framing(txn.req_hdrs, &txn.req_body,
&txn.error.desc))) {
ret = r;
}
/* Check for Expectations */
else if ((r = parse_expect(&txn))) {
ret = r;
}
/* Check for mandatory Host header (HTTP/1.1+ only) */
else if ((hdr = spool_getheader(txn.req_hdrs, "Host")) && hdr[1]) {
ret = HTTP_BAD_REQUEST;
txn.error.desc = "Too many Host headers";
}
else if (!hdr) {
if (txn.flags.ver1_0) {
/* HTTP/1.0 - create a Host header from URI */
if (txn.req_uri->server) {
buf_setcstr(&txn.buf, txn.req_uri->server);
if (txn.req_uri->port)
buf_printf(&txn.buf, ":%d", txn.req_uri->port);
}
else buf_setcstr(&txn.buf, config_servername);
spool_cache_header(xstrdup("Host"),
xstrdup(buf_cstring(&txn.buf)),
txn.req_hdrs);
buf_reset(&txn.buf);
}
else {
ret = HTTP_BAD_REQUEST;
txn.error.desc = "Missing Host header";
}
}
if (ret) goto done;
query = URI_QUERY(txn.req_uri);
/* Find the namespace of the requested resource */
for (i = 0; namespaces[i]; i++) {
const char *path = txn.req_uri->path;
size_t len;
/* Skip disabled namespaces */
if (!namespaces[i]->enabled) continue;
/* Handle any /.well-known/ bootstrapping */
if (namespaces[i]->well_known) {
len = strlen(namespaces[i]->well_known);
if (!strncmp(path, namespaces[i]->well_known, len) &&
(!path[len] || path[len] == '/')) {
hdr = spool_getheader(txn.req_hdrs, "Host");
buf_reset(&txn.buf);
buf_printf(&txn.buf, "%s://%s",
https? "https" : "http", hdr[0]);
buf_appendcstr(&txn.buf, namespaces[i]->prefix);
buf_appendcstr(&txn.buf, path + len);
if (query) buf_printf(&txn.buf, "?%s", query);
txn.location = buf_cstring(&txn.buf);
ret = HTTP_MOVED;
goto done;
}
}
/* See if the prefix matches - terminated with NUL or '/' */
len = strlen(namespaces[i]->prefix);
if (!strncmp(path, namespaces[i]->prefix, len) &&
(!path[len] || (path[len] == '/') || !strcmp(path, "*"))) {
break;
}
}
if ((namespace = namespaces[i])) {
txn.req_tgt.namespace = namespace->id;
txn.req_tgt.allow = namespace->allow;
txn.req_tgt.mboxtype = namespace->mboxtype;
/* Check if method is supported in this namespace */
meth_t = &namespace->methods[txn.meth];
if (!meth_t->proc) ret = HTTP_NOT_ALLOWED;
/* Check if method expects a body */
else if ((http_methods[txn.meth].flags & METH_NOBODY) &&
(txn.req_body.framing != FRAMING_LENGTH ||
/* XXX Will break if client sends just a last-chunk */
txn.req_body.len)) {
ret = HTTP_BAD_MEDIATYPE;
}
} else {
/* XXX Should never get here */
ret = HTTP_SERVER_ERROR;
}
if (ret) goto done;
/* Perform authentication, if necessary */
if ((hdr = spool_getheader(txn.req_hdrs, "Authorization"))) {
if (httpd_userid) {
/* Reauth - reinitialize */
syslog(LOG_DEBUG, "reauth - reinit");
reset_saslconn(&httpd_saslconn);
txn.auth_chal.scheme = NULL;
}
/* Check the auth credentials */
r = http_auth(hdr[0], &txn);
if ((r < 0) || !txn.auth_chal.scheme) {
/* Auth failed - reinitialize */
syslog(LOG_DEBUG, "auth failed - reinit");
reset_saslconn(&httpd_saslconn);
txn.auth_chal.scheme = NULL;
ret = HTTP_UNAUTHORIZED;
}
}
else if (!httpd_userid && txn.auth_chal.scheme) {
/* Started auth exchange, but client didn't engage - reinit */
syslog(LOG_DEBUG, "client didn't complete auth - reinit");
reset_saslconn(&httpd_saslconn);
txn.auth_chal.scheme = NULL;
}
/* Perform proxy authorization, if necessary */
else if (saslprops.authid &&
(hdr = spool_getheader(txn.req_hdrs, "Authorize-As")) &&
*hdr[0]) {
const char *authzid = hdr[0];
r = proxy_authz(&authzid, &txn);
if (r) {
/* Proxy authz failed - reinitialize */
syslog(LOG_DEBUG, "proxy authz failed - reinit");
reset_saslconn(&httpd_saslconn);
txn.auth_chal.scheme = NULL;
ret = HTTP_UNAUTHORIZED;
}
else {
httpd_userid = xstrdup(authzid);
auth_success(&txn);
}
}
/* Request authentication, if necessary */
switch (txn.meth) {
case METH_GET:
case METH_HEAD:
case METH_OPTIONS:
/* Let method processing function decide if auth is needed */
break;
default:
if (!httpd_userid && namespace->need_auth) {
/* Authentication required */
ret = HTTP_UNAUTHORIZED;
}
}
if (ret) goto need_auth;
/* Check if this is a Cross-Origin Resource Sharing request */
if (allow_cors && (hdr = spool_getheader(txn.req_hdrs, "Origin"))) {
const char *err = NULL;
xmlURIPtr uri = parse_uri(METH_UNKNOWN, hdr[0], 0, &err);
if (uri && uri->scheme && uri->server) {
int o_https = !strcasecmp(uri->scheme, "https");
if ((https == o_https) &&
!strcasecmp(uri->server,
*spool_getheader(txn.req_hdrs, "Host"))) {
txn.flags.cors = CORS_SIMPLE;
}
else {
struct wildmat *wild;
/* Create URI w/o path or default port */
assert(!buf_len(&txn.buf));
buf_printf(&txn.buf, "%s://%s",
lcase(uri->scheme), lcase(uri->server));
if (uri->port &&
((o_https && uri->port != 443) ||
(!o_https && uri->port != 80))) {
buf_printf(&txn.buf, ":%d", uri->port);
}
/* Check Origin against the 'httpallowcors' wildmat */
for (wild = allow_cors; wild->pat; wild++) {
if (wildmat(buf_cstring(&txn.buf), wild->pat)) {
/* If we have a non-negative match, allow request */
if (!wild->not) txn.flags.cors = CORS_SIMPLE;
break;
}
}
buf_reset(&txn.buf);
}
}
xmlFreeURI(uri);
}
/* Check if we should compress response body */
if (gzip_enabled) {
/* XXX Do we want to support deflate even though M$
doesn't implement it correctly (raw deflate vs. zlib)? */
if (!txn.flags.ver1_0 &&
(hdr = spool_getheader(txn.req_hdrs, "TE"))) {
struct accept *e, *enc = parse_accept(hdr);
for (e = enc; e && e->token; e++) {
if (e->qual > 0.0 &&
(!strcasecmp(e->token, "gzip") ||
!strcasecmp(e->token, "x-gzip"))) {
txn.flags.te = TE_GZIP;
}
free(e->token);
}
if (enc) free(enc);
}
else if ((hdr = spool_getheader(txn.req_hdrs, "Accept-Encoding"))) {
struct accept *e, *enc = parse_accept(hdr);
for (e = enc; e && e->token; e++) {
if (e->qual > 0.0 &&
(!strcasecmp(e->token, "gzip") ||
!strcasecmp(e->token, "x-gzip"))) {
txn.resp_body.enc = CE_GZIP;
}
free(e->token);
}
if (enc) free(enc);
}
}
/* Parse any query parameters */
if (query) {
/* Parse the query string and add key/value pairs to hash table */
tok_t tok;
char *param;
assert(!buf_len(&txn.buf)); /* Unescape buffer */
tok_init(&tok, (char *) query, ";&",
TOK_TRIMLEFT|TOK_TRIMRIGHT|TOK_EMPTY);
while ((param = tok_next(&tok))) {
struct strlist *vals;
char *key, *value;
tok_t tok2;
size_t len;
/* Split param into key and optional value */
tok_initm(&tok2, param, "=",
TOK_TRIMLEFT|TOK_TRIMRIGHT|TOK_EMPTY);
key = tok_next(&tok2);
value = tok_next(&tok2);
if (!value) value = "";
len = strlen(value);
buf_ensure(&txn.buf, len+1);
vals = hash_lookup(key, &txn.req_qparams);
appendstrlist(&vals,
xmlURIUnescapeString(value, len, txn.buf.s));
hash_insert(key, vals, &txn.req_qparams);
tok_fini(&tok2);
}
tok_fini(&tok);
buf_reset(&txn.buf);
}
/* Start method processing alarm (HTTP/1.1+ only) */
if (!txn.flags.ver1_0) alarm(httpd_keepalive);
/* Process the requested method */
ret = (*meth_t->proc)(&txn, meth_t->params);
need_auth:
if (ret == HTTP_UNAUTHORIZED) {
/* User must authenticate */
if (httpd_tls_required) {
/* We only support TLS+Basic, so tell client to use TLS */
ret = 0;
/* Check which response is required */
if ((hdr = spool_getheader(txn.req_hdrs, "Upgrade")) &&
!strncmp(hdr[0], TLS_VERSION, strcspn(hdr[0], " ,"))) {
/* Client (Murder proxy) supports RFC 2817 (TLS upgrade) */
response_header(HTTP_UPGRADE, &txn);
}
else {
/* All other clients use RFC 2818 (HTTPS) */
const char *path = txn.req_uri->path;
struct buf *html = &txn.resp_body.payload;
/* Create https URL */
hdr = spool_getheader(txn.req_hdrs, "Host");
buf_printf(&txn.buf, "https://%s", hdr[0]);
if (strcmp(path, "*")) {
buf_appendcstr(&txn.buf, path);
if (query) buf_printf(&txn.buf, "?%s", query);
}
txn.location = buf_cstring(&txn.buf);
/* Create HTML body */
buf_reset(html);
buf_printf(html, tls_message,
buf_cstring(&txn.buf), buf_cstring(&txn.buf));
/* Output our HTML response */
txn.resp_body.type = "text/html; charset=utf-8";
write_body(HTTP_MOVED, &txn,
buf_cstring(html), buf_len(html));
}
}
else {
/* Tell client to authenticate */
if (r == SASL_CONTINUE)
txn.error.desc = "Continue authentication exchange";
else if (r) txn.error.desc = "Authentication failed";
else txn.error.desc =
"Must authenticate to access the specified target";
}
}
done:
/* Handle errors (success responses handled by method functions) */
if (ret) error_response(ret, &txn);
/* Read and discard any unread request body */
if (!(txn.flags.conn & CONN_CLOSE)) {
txn.req_body.flags |= BODY_DISCARD;
if (http_read_body(httpd_in, httpd_out,
txn.req_hdrs, &txn.req_body, &txn.error.desc)) {
txn.flags.conn = CONN_CLOSE;
}
}
/* Memory cleanup */
if (txn.req_uri) xmlFreeURI(txn.req_uri);
if (txn.req_hdrs) spool_free_hdrcache(txn.req_hdrs);
free_hash_table(&txn.req_qparams, (void (*)(void *)) &freestrlist);
/* XXX - split this into a req_tgt cleanup */
free(txn.req_tgt.userid);
mboxlist_entry_free(&txn.req_tgt.mbentry);
if (txn.flags.conn & CONN_CLOSE) {
buf_free(&txn.buf);
buf_free(&txn.req_body.payload);
buf_free(&txn.resp_body.payload);
#ifdef HAVE_ZLIB
deflateEnd(&txn.zstrm);
buf_free(&txn.zbuf);
#endif
return;
}
continue;
}
}
/**************************** Parsing Routines ******************************/
/* Parse URI, returning the path */
EXPORTED xmlURIPtr parse_uri(unsigned meth, const char *uri, unsigned path_reqd,
const char **errstr)
{
xmlURIPtr p_uri; /* parsed URI */
/* Parse entire URI */
if ((p_uri = xmlParseURI(uri)) == NULL) {
*errstr = "Illegal request target URI";
goto bad_request;
}
if (p_uri->scheme) {
/* Check sanity of scheme */
if (strcasecmp(p_uri->scheme, "http") &&
strcasecmp(p_uri->scheme, "https")) {
*errstr = "Unsupported URI scheme";
goto bad_request;
}
}
/* Check sanity of path */
if (path_reqd && (!p_uri->path || !*p_uri->path)) {
*errstr = "Empty path in target URI";
goto bad_request;
}
else if (p_uri->path) {
if ((p_uri->path[0] != '/') &&
(strcmp(p_uri->path, "*") || (meth != METH_OPTIONS))) {
/* No special URLs except for "OPTIONS * HTTP/1.1" */
*errstr = "Illegal request target URI";
goto bad_request;
}
else if (strstr(p_uri->path, "/..")) {
/* Don't allow access up directory tree */
*errstr = "Illegal request target URI";
goto bad_request;
}
else if (strlen(p_uri->path) > MAX_MAILBOX_PATH) {
*errstr = "Request target URI too long";
goto bad_request;
}
}
return p_uri;
bad_request:
if (p_uri) xmlFreeURI(p_uri);
return NULL;
}
/* Calculate compile time of a file for use as Last-Modified and/or ETag */
EXPORTED time_t calc_compile_time(const char *time, const char *date)
{
struct tm tm;
char month[4];
const char *monthname[] = {
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
};
memset(&tm, 0, sizeof(struct tm));
tm.tm_isdst = -1;
sscanf(time, "%02d:%02d:%02d", &tm.tm_hour, &tm.tm_min, &tm.tm_sec);
sscanf(date, "%s %2d %4d", month, &tm.tm_mday, &tm.tm_year);
tm.tm_year -= 1900;
for (tm.tm_mon = 0; tm.tm_mon < 12; tm.tm_mon++) {
if (!strcmp(month, monthname[tm.tm_mon])) break;
}
return mktime(&tm);
}
/* Parse Expect header(s) for interesting expectations */
static int parse_expect(struct transaction_t *txn)
{
const char **exp = spool_getheader(txn->req_hdrs, "Expect");
int i, ret = 0;
/* Expect not supported by HTTP/1.0 clients */
if (exp && txn->flags.ver1_0) return HTTP_EXPECT_FAILED;
/* Look for interesting expectations. Unknown == error */
for (i = 0; !ret && exp && exp[i]; i++) {
tok_t tok = TOK_INITIALIZER(exp[i], ",", TOK_TRIMLEFT|TOK_TRIMRIGHT);
char *token;
while (!ret && (token = tok_next(&tok))) {
/* Check if this is a non-persistent connection */
if (!strcasecmp(token, "100-continue")) {
syslog(LOG_DEBUG, "Expect: 100-continue");
txn->req_body.flags |= BODY_CONTINUE;
}
else {
txn->error.desc = "Unsupported Expectation";
ret = HTTP_EXPECT_FAILED;
}
}
tok_fini(&tok);
}
return ret;
}
/* Parse Connection header(s) for interesting options */
static void parse_connection(struct transaction_t *txn)
{
const char **conn = spool_getheader(txn->req_hdrs, "Connection");
int i;
/* Look for interesting connection tokens */
for (i = 0; conn && conn[i]; i++) {
tok_t tok = TOK_INITIALIZER(conn[i], ",", TOK_TRIMLEFT|TOK_TRIMRIGHT);
char *token;
while ((token = tok_next(&tok))) {
if (httpd_timeout) {
/* Check if this is a non-persistent connection */
if (!strcasecmp(token, "close")) {
txn->flags.conn |= CONN_CLOSE;
continue;
}
/* Check if this is a persistent connection */
else if (!strcasecmp(token, "keep-alive")) {
txn->flags.conn |= CONN_KEEPALIVE;
continue;
}
}
/* Check if we need to upgrade to TLS */
if (!httpd_tls_done && tls_enabled() &&
!strcasecmp(token, "Upgrade")) {
const char **upgrd;
if ((upgrd = spool_getheader(txn->req_hdrs, "Upgrade")) &&
!strncmp(upgrd[0], TLS_VERSION, strcspn(upgrd[0], " ,"))) {
syslog(LOG_DEBUG, "client requested TLS");
txn->flags.conn |= CONN_UPGRADE;
}
}
}
tok_fini(&tok);
}
if (!httpd_timeout) txn->flags.conn |= CONN_CLOSE;
else if (txn->flags.conn & CONN_CLOSE) {
/* close overrides keep-alive */
txn->flags.conn &= ~CONN_KEEPALIVE;
}
else if (txn->flags.ver1_0 && !(txn->flags.conn & CONN_KEEPALIVE)) {
/* HTTP/1.0 - non-persistent connection unless keep-alive */
txn->flags.conn |= CONN_CLOSE;
}
}
/* Compare accept quality values so that they sort in descending order */
static int compare_accept(const struct accept *a1, const struct accept *a2)
{
if (a2->qual < a1->qual) return -1;
if (a2->qual > a1->qual) return 1;
return 0;
}
struct accept *parse_accept(const char **hdr)
{
int i, n = 0, alloc = 0;
struct accept *ret = NULL;
#define GROW_ACCEPT 10;
for (i = 0; hdr && hdr[i]; i++) {
tok_t tok = TOK_INITIALIZER(hdr[i], ";,", TOK_TRIMLEFT|TOK_TRIMRIGHT);
char *token;
while ((token = tok_next(&tok))) {
if (!strncmp(token, "q=", 2)) {
if (!ret) break;
ret[n-1].qual = strtof(token+2, NULL);
}
else {
if (n + 1 >= alloc) {
alloc += GROW_ACCEPT;
ret = xrealloc(ret, alloc * sizeof(struct accept));
}
ret[n].token = xstrdup(token);
ret[n].qual = 1.0;
ret[++n].token = NULL;
}
}
tok_fini(&tok);
}
qsort(ret, n, sizeof(struct accept),
(int (*)(const void *, const void *)) &compare_accept);
return ret;
}
/**************************** Response Routines *****************************/
/* Create HTTP-date ('buf' must be at least 30 characters) */
EXPORTED char *httpdate_gen(char *buf, size_t len, time_t t)
{
static char *month[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
static char *wday[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
struct tm *tm = gmtime(&t);
snprintf(buf, len, "%3s, %02d %3s %4d %02d:%02d:%02d GMT",
wday[tm->tm_wday],
tm->tm_mday, month[tm->tm_mon], tm->tm_year + 1900,
tm->tm_hour, tm->tm_min, tm->tm_sec);
return buf;
}
/* Create an HTTP Status-Line given response code */
EXPORTED const char *http_statusline(long code)
{
static struct buf statline = BUF_INITIALIZER;
static unsigned tail = 0;
if (!tail) {
buf_setcstr(&statline, HTTP_VERSION);
buf_putc(&statline, ' ');
tail = buf_len(&statline);
}
buf_truncate(&statline, tail);
buf_appendcstr(&statline, error_message(code));
return buf_cstring(&statline);
}
/* Output an HTTP response header.
* 'code' specifies the HTTP Status-Code and Reason-Phrase.
* 'txn' contains the transaction context
*/
#define WWW_Authenticate(name, param) \
prot_printf(httpd_out, "WWW-Authenticate: %s", name); \
if (param) prot_printf(httpd_out, " %s", param); \
prot_puts(httpd_out, "\r\n")
#define Access_Control_Expose(hdr) \
prot_puts(httpd_out, "Access-Control-Expose-Headers: " hdr "\r\n")
EXPORTED void comma_list_hdr(const char *hdr, const char *vals[], unsigned flags, ...)
{
const char *sep = " ";
va_list args;
int i;
va_start(args, flags);
prot_printf(httpd_out, "%s:", hdr);
for (i = 0; vals[i]; i++) {
if (flags & (1 << i)) {
prot_puts(httpd_out, sep);
prot_vprintf(httpd_out, vals[i], args);
sep = ", ";
}
else {
/* discard any unused args */
vsnprintf(NULL, 0, vals[i], args);
}
}
prot_puts(httpd_out, "\r\n");
va_end(args);
}
EXPORTED void allow_hdr(const char *hdr, unsigned allow)
{
const char *meths[] = {
"OPTIONS, GET, HEAD", "POST", "PUT", "DELETE", "TRACE", NULL
};
comma_list_hdr(hdr, meths, allow);
if (allow & ALLOW_DAV) {
prot_printf(httpd_out, "%s: PROPFIND, REPORT", hdr);
if (allow & ALLOW_WRITE) {
prot_puts(httpd_out, ", COPY, MOVE, LOCK, UNLOCK");
}
if (allow & ALLOW_WRITECOL) {
prot_puts(httpd_out, ", PROPPATCH, MKCOL, ACL");
if (allow & ALLOW_CAL) {
prot_printf(httpd_out, "\r\n%s: MKCALENDAR", hdr);
}
}
prot_puts(httpd_out, "\r\n");
}
}
#define MD5_BASE64_LEN 25 /* ((MD5_DIGEST_LENGTH / 3) + 1) * 4 */
EXPORTED void Content_MD5(const unsigned char *md5)
{
char base64[MD5_BASE64_LEN+1];
sasl_encode64((char *) md5, MD5_DIGEST_LENGTH,
base64, MD5_BASE64_LEN, NULL);
prot_printf(httpd_out, "Content-MD5: %s\r\n", base64);
}
EXPORTED void response_header(long code, struct transaction_t *txn)
{
time_t now;
char datestr[30];
unsigned keepalive;
const char **hdr;
struct auth_challenge_t *auth_chal;
struct resp_body_t *resp_body;
static struct buf log = BUF_INITIALIZER;
/* Stop method processing alarm */
keepalive = alarm(0);
/* Status-Line */
prot_printf(httpd_out, "%s\r\n", http_statusline(code));
/* Connection Management */
switch (code) {
case HTTP_SWITCH_PROT:
keepalive = 0; /* No alarm during TLS negotiation */
prot_printf(httpd_out, "Upgrade: %s\r\n", TLS_VERSION);
prot_puts(httpd_out, "Connection: Upgrade\r\n");
/* Fall through as provisional response */
case HTTP_CONTINUE:
case HTTP_PROCESSING:
/* Provisional response - nothing else needed */
/* CRLF terminating the header block */
prot_puts(httpd_out, "\r\n");
/* Force the response to the client immediately */
prot_flush(httpd_out);
/* Reset method processing alarm */
alarm(keepalive);
return;
case HTTP_UPGRADE:
txn->flags.conn |= CONN_UPGRADE;
prot_printf(httpd_out, "Upgrade: %s\r\n", TLS_VERSION);
/* Fall through as final response */
default:
/* Final response */
if (txn->flags.conn) {
/* Construct Connection header */
const char *conn_tokens[] =
{ "close", "Upgrade", "Keep-Alive", NULL };
if (txn->flags.conn & CONN_KEEPALIVE) {
prot_printf(httpd_out, "Keep-Alive: timeout=%d\r\n",
httpd_timeout);
}
comma_list_hdr("Connection", conn_tokens, txn->flags.conn);
}
auth_chal = &txn->auth_chal;
resp_body = &txn->resp_body;
}
/* Control Data */
now = time(0);
httpdate_gen(datestr, sizeof(datestr), now);
prot_printf(httpd_out, "Date: %s\r\n", datestr);
if (httpd_tls_done) {
prot_puts(httpd_out, "Strict-Transport-Security: max-age=600\r\n");
}
if (txn->location) {
prot_printf(httpd_out, "Location: %s\r\n", txn->location);
}
if (txn->flags.cc) {
/* Construct Cache-Control header */
const char *cc_dirs[] =
{ "must-revalidate", "no-cache", "no-store", "no-transform",
"public", "private", "max-age=%d", NULL };
comma_list_hdr("Cache-Control", cc_dirs, txn->flags.cc,
resp_body->maxage);
if (txn->flags.cc & CC_MAXAGE) {
httpdate_gen(datestr, sizeof(datestr), now + resp_body->maxage);
prot_printf(httpd_out, "Expires: %s\r\n", datestr);
}
}
if (txn->flags.cors) {
/* Construct Cross-Origin Resource Sharing headers */
prot_printf(httpd_out, "Access-Control-Allow-Origin: %s\r\n",
*spool_getheader(txn->req_hdrs, "Origin"));
prot_puts(httpd_out, "Access-Control-Allow-Credentials: true\r\n");
if (txn->flags.cors == CORS_PREFLIGHT) {
allow_hdr("Access-Control-Allow-Methods", txn->req_tgt.allow);
for (hdr = spool_getheader(txn->req_hdrs,
"Access-Control-Request-Headers");
hdr && *hdr; hdr++) {
prot_printf(httpd_out,
"Access-Control-Allow-Headers: %s\r\n", *hdr);
}
prot_puts(httpd_out, "Access-Control-Max-Age: 3600\r\n");
}
}
if (txn->flags.vary) {
/* Construct Vary header */
const char *vary_hdrs[] =
{ "Accept", "Accept-Encoding", "Brief", "Prefer", NULL };
comma_list_hdr("Vary", vary_hdrs, txn->flags.vary);
}
/* Response Context */
if (txn->flags.mime) {
prot_puts(httpd_out, "MIME-Version: 1.0\r\n");
}
if (txn->req_tgt.allow & ALLOW_ISCHEDULE) {
prot_puts(httpd_out, "iSchedule-Version: 1.0\r\n");
if (resp_body->iserial) {
prot_printf(httpd_out, "iSchedule-Capabilities: %ld\r\n",
resp_body->iserial);
}
}
if (resp_body->cmid) {
prot_printf(httpd_out, "Cal-Managed-ID: \"%s\"\r\n", resp_body->cmid);
if (txn->flags.cors) Access_Control_Expose("Cal-Managed-ID");
}
if (resp_body->prefs) {
/* Construct Preference-Applied header */
const char *prefs[] =
{ "return=minimal", "return=representation", "depth-noroot", NULL };
comma_list_hdr("Preference-Applied", prefs, resp_body->prefs);
if (txn->flags.cors) Access_Control_Expose("Preference-Applied");
}
switch (code) {
case HTTP_OK:
switch (txn->meth) {
case METH_GET:
case METH_HEAD:
/* Construct Accept-Ranges header for GET and HEAD responses */
prot_printf(httpd_out, "Accept-Ranges: %s\r\n",
txn->flags.ranges ? "bytes" : "none");
break;
case METH_OPTIONS:
if (config_serverinfo == IMAP_ENUM_SERVERINFO_ON) {
prot_printf(httpd_out, "Server: %s\r\n",
buf_cstring(&serverinfo));
}
if (txn->req_tgt.allow & ALLOW_DAV) {
/* Construct DAV header(s) based on namespace of request URL */
prot_printf(httpd_out, "DAV: 1,%s 3, access-control%s\r\n",
(txn->req_tgt.allow & ALLOW_WRITE) ? " 2," : "",
(txn->req_tgt.allow & ALLOW_WRITECOL) ?
", extended-mkcol" : "");
if (txn->req_tgt.allow & ALLOW_CAL) {
prot_printf(httpd_out, "DAV: calendar-access%s%s%s\r\n",
(txn->req_tgt.allow & ALLOW_CAL_AVAIL) ?
", calendar-availability" : "",
(txn->req_tgt.allow & ALLOW_CAL_SCHED) ?
", calendar-auto-schedule" : "",
(txn->req_tgt.allow & ALLOW_CAL_NOTZ) ?
", calendar-no-timezone" : "");
if (txn->req_tgt.allow & ALLOW_CAL_ATTACH) {
prot_puts(httpd_out,
"DAV: calendar-managed-attachments, "
"calendar-managed-attachments-no-recurrence\r\n");
}
/* Backwards compatibility with older Apple VAV clients */
if ((txn->req_tgt.allow &
(ALLOW_CAL_AVAIL | ALLOW_CAL_SCHED)) ==
(ALLOW_CAL_AVAIL | ALLOW_CAL_SCHED)) {
if ((hdr = spool_getheader(txn->req_hdrs, "User-Agent"))
&& strstr(hdr[0], "CalendarAgent/")) {
prot_puts(httpd_out, "DAV: inbox-availability\r\n");
}
}
}
if (txn->req_tgt.allow & ALLOW_CARD) {
prot_puts(httpd_out, "DAV: addressbook\r\n");
}
}
if (txn->flags.cors == CORS_PREFLIGHT) {
/* Access-Control-Allow-Methods supersedes Allow */
break;
}
else goto allow;
}
goto authorized;
case HTTP_NOT_ALLOWED:
allow:
/* Construct Allow header(s) for OPTIONS and 405 response */
allow_hdr("Allow", txn->req_tgt.allow);
goto authorized;
case HTTP_BAD_CE:
/* Construct Allow-Encoding header for 415 response */
#ifdef HAVE_ZLIB
prot_puts(httpd_out, "Allow-Encoding: gzip, deflate\r\n");
#else
prot_puts(httpd_out, "Allow-Encoding: identity\r\n");
#endif
goto authorized;
case HTTP_UNAUTHORIZED:
/* Authentication Challenges */
if (!auth_chal->scheme) {
/* Require authentication by advertising all possible schemes */
struct auth_scheme_t *scheme;
for (scheme = auth_schemes; scheme->name; scheme++) {
/* Only advertise what is available and
can work with the type of connection */
if ((avail_auth_schemes & (1 << scheme->idx)) &&
!((txn->flags.conn & CONN_CLOSE) &&
(scheme->flags & AUTH_NEED_PERSIST))) {
auth_chal->param = NULL;
if (scheme->flags & AUTH_SERVER_FIRST) {
/* Generate the initial challenge */
http_auth(scheme->name, txn);
if (!auth_chal->param) continue; /* If fail, skip it */
}
WWW_Authenticate(scheme->name, auth_chal->param);
}
}
}
else {
/* Continue with current authentication exchange */
WWW_Authenticate(auth_chal->scheme->name, auth_chal->param);
}
break;
default:
authorized:
/* Authentication completed/unnecessary */
if (auth_chal->param) {
/* Authentication completed with success data */
if (auth_chal->scheme->send_success) {
/* Special handling of success data for this scheme */
auth_chal->scheme->send_success(auth_chal->scheme->name,
auth_chal->param);
}
else {
/* Default handling of success data */
WWW_Authenticate(auth_chal->scheme->name, auth_chal->param);
}
}
}
/* Validators */
if (resp_body->lock) {
prot_printf(httpd_out, "Lock-Token: <%s>\r\n", resp_body->lock);
if (txn->flags.cors) Access_Control_Expose("Lock-Token");
}
if (resp_body->stag) {
prot_printf(httpd_out, "Schedule-Tag: \"%s\"\r\n", resp_body->stag);
if (txn->flags.cors) Access_Control_Expose("Schedule-Tag");
}
if (resp_body->etag) {
prot_printf(httpd_out, "ETag: %s\"%s\"\r\n",
resp_body->enc ? "W/" : "", resp_body->etag);
if (txn->flags.cors) Access_Control_Expose("ETag");
}
if (resp_body->lastmod) {
/* Last-Modified MUST NOT be in the future */
resp_body->lastmod = MIN(resp_body->lastmod, now);
httpdate_gen(datestr, sizeof(datestr), resp_body->lastmod);
prot_printf(httpd_out, "Last-Modified: %s\r\n", datestr);
}
/* Representation Metadata */
if (resp_body->type) {
prot_printf(httpd_out, "Content-Type: %s\r\n", resp_body->type);
if (resp_body->fname) {
prot_printf(httpd_out,
"Content-Disposition: attachment; filename=\"%s\"\r\n",
resp_body->fname);
}
if (txn->resp_body.enc) {
/* Construct Content-Encoding header */
const char *ce[] =
{ "deflate", "gzip", NULL };
comma_list_hdr("Content-Encoding", ce, txn->resp_body.enc);
}
if (resp_body->lang) {
prot_printf(httpd_out, "Content-Language: %s\r\n", resp_body->lang);
}
if (resp_body->loc) {
prot_printf(httpd_out, "Content-Location: %s\r\n", resp_body->loc);
if (txn->flags.cors) Access_Control_Expose("Content-Location");
}
if (resp_body->md5) {
Content_MD5(resp_body->md5);
}
}
/* Payload */
switch (code) {
case HTTP_NO_CONTENT:
case HTTP_NOT_MODIFIED:
/* MUST NOT include a body */
break;
case HTTP_BAD_RANGE:
prot_printf(httpd_out, "Content-Range: bytes */%lu\r\n",
resp_body->len);
resp_body->len = 0; /* No content */
/* Fall through and specify framing */
case HTTP_PARTIAL:
if (resp_body->range) {
prot_printf(httpd_out, "Content-Range: bytes %lu-%lu/%lu\r\n",
resp_body->range->first, resp_body->range->last,
resp_body->len);
/* Set actual content length of range */
resp_body->len = resp_body->range->last -
resp_body->range->first + 1;
free(resp_body->range);
}
/* Fall through and specify framing */
default:
if (txn->flags.te) {
/* HTTP/1.1+ only - we use close-delimiting for HTTP/1.0 */
if (!txn->flags.ver1_0) {
/* Construct Transfer-Encoding header */
const char *te[] =
{ "deflate", "gzip", "chunked", NULL };
comma_list_hdr("Transfer-Encoding", te, txn->flags.te);
if (txn->flags.trailer) {
/* Construct Trailer header */
const char *trailer_hdrs[] =
{ "Content-MD5", NULL };
comma_list_hdr("Trailer", trailer_hdrs, txn->flags.trailer);
}
}
}
else if (resp_body->len || txn->meth != METH_HEAD) {
prot_printf(httpd_out, "Content-Length: %lu\r\n", resp_body->len);
}
}
/* CRLF terminating the header block */
prot_puts(httpd_out, "\r\n");
/* Log the client request and our response */
buf_reset(&log);
/* Add client data */
buf_printf(&log, "%s", httpd_clienthost);
if (proxy_userid) buf_printf(&log, " as \"%s\"", proxy_userid);
if (txn->req_hdrs &&
(hdr = spool_getheader(txn->req_hdrs, "User-Agent"))) {
buf_printf(&log, " with \"%s\"", hdr[0]);
if ((hdr = spool_getheader(txn->req_hdrs, "X-Client")))
buf_printf(&log, " by \"%s\"", hdr[0]);
else if ((hdr = spool_getheader(txn->req_hdrs, "X-Requested-With")))
buf_printf(&log, " by \"%s\"", hdr[0]);
}
/* Add request-line */
buf_appendcstr(&log, "; \"");
if (txn->req_line.meth) {
buf_printf(&log, "%s",
txn->flags.override ? "POST" : txn->req_line.meth);
if (txn->req_line.uri) {
buf_printf(&log, " %s", txn->req_line.uri);
if (txn->req_line.ver) {
buf_printf(&log, " %s", txn->req_line.ver);
if (code != HTTP_URI_TOO_LONG) {
char *p = txn->req_line.ver + strlen(txn->req_line.ver) + 1;
if (*p) buf_printf(&log, " %s", p);
}
}
}
}
buf_appendcstr(&log, "\"");
if (txn->req_hdrs) {
/* Add any request modifying headers */
const char *sep = " (";
if (txn->flags.override) {
buf_printf(&log, "%smethod-override=%s", sep, txn->req_line.meth);
sep = "; ";
}
if ((hdr = spool_getheader(txn->req_hdrs, "Origin"))) {
buf_printf(&log, "%sorigin=%s", sep, hdr[0]);
sep = "; ";
}
if ((hdr = spool_getheader(txn->req_hdrs, "Referer"))) {
buf_printf(&log, "%sreferer=%s", sep, hdr[0]);
sep = "; ";
}
if ((hdr = spool_getheader(txn->req_hdrs, "Destination"))) {
buf_printf(&log, "%sdestination=%s", sep, hdr[0]);
sep = "; ";
}
if ((hdr = spool_getheader(txn->req_hdrs, ":type"))) {
buf_printf(&log, "%stype=%s", sep, hdr[0]);
sep = "; ";
}
if ((hdr = spool_getheader(txn->req_hdrs, "Depth"))) {
buf_printf(&log, "%sdepth=%s", sep, hdr[0]);
sep = "; ";
}
if (*sep == ';') buf_appendcstr(&log, ")");
}
buf_printf(&log, " => \"%s\"", error_message(code));
/* Add any auxiliary response data */
if (txn->location) {
buf_printf(&log, " (location=%s)", txn->location);
}
else if (txn->flags.cors) {
buf_appendcstr(&log, " (allow-origin)");
}
else if (txn->error.desc) {
buf_printf(&log, " (error=%s)", txn->error.desc);
}
syslog(LOG_INFO, "%s", buf_cstring(&log));
}
static void keep_alive(int sig)
{
if (sig == SIGALRM) {
response_header(HTTP_CONTINUE, NULL);
alarm(httpd_keepalive);
}
}
/*
* Output an HTTP response with multipart body data.
*
* An initial call with 'code' != 0 will output a response header
* and the preamble.
* All subsequent calls should have 'code' = 0 to output just a body part.
* A final call with 'len' = 0 ends the multipart body.
*/
EXPORTED void write_multipart_body(long code, struct transaction_t *txn,
const char *buf, unsigned len)
{
static char boundary[100];
struct buf *body = &txn->resp_body.payload;
if (code) {
const char *preamble =
"This is a message with multiple parts in MIME format.\r\n";
txn->flags.mime = 1;
/* Create multipart boundary */
snprintf(boundary, sizeof(boundary), "%s-%ld-%ld-%ld",
*spool_getheader(txn->req_hdrs, "Host"),
(long) getpid(), (long) time(0), (long) rand());
/* Create Content-Type w/ boundary */
assert(!buf_len(&txn->buf));
buf_printf(&txn->buf, "%s; boundary=\"%s\"",
txn->resp_body.type, boundary);
txn->resp_body.type = buf_cstring(&txn->buf);
/* Setup for chunked response and begin multipart */
txn->flags.te |= TE_CHUNKED;
if (!buf) {
buf = preamble;
len = strlen(preamble);
}
write_body(code, txn, buf, len);
}
else if (len) {
/* Output delimiter and MIME part-headers */
buf_reset(body);
buf_printf(body, "\r\n--%s\r\n", boundary);
buf_printf(body, "Content-Type: %s\r\n", txn->resp_body.type);
if (txn->resp_body.range) {
buf_printf(body, "Content-Range: bytes %lu-%lu/%lu\r\n",
txn->resp_body.range->first,
txn->resp_body.range->last,
txn->resp_body.len);
}
buf_printf(body, "Content-Length: %d\r\n\r\n", len);
write_body(0, txn, buf_cstring(body), buf_len(body));
/* Output body-part data */
write_body(0, txn, buf, len);
}
else {
const char *epilogue = "\r\nEnd of MIME multipart body.\r\n";
/* Output close-delimiter and epilogue */
buf_reset(body);
buf_printf(body, "\r\n--%s--\r\n%s", boundary, epilogue);
write_body(0, txn, buf_cstring(body), buf_len(body));
/* End of output */
write_body(0, txn, NULL, 0);
}
}
/* Output multipart/byteranges */
static void multipart_byteranges(struct transaction_t *txn,
const char *msg_base)
{
/* Save Content-Range and Content-Type pointers */
struct range *range = txn->resp_body.range;
const char *type = txn->resp_body.type;
/* Start multipart response */
txn->resp_body.range = NULL;
txn->resp_body.type = "multipart/byteranges";
write_multipart_body(HTTP_PARTIAL, txn, NULL, 0);
txn->resp_body.type = type;
while (range) {
unsigned long offset = range->first;
unsigned long datalen = range->last - range->first + 1;
struct range *next = range->next;
/* Output range as body part */
txn->resp_body.range = range;
write_multipart_body(0, txn, msg_base + offset, datalen);
/* Cleanup */
free(range);
range = next;
}
/* End of multipart body */
write_multipart_body(0, txn, NULL, 0);
}
/*
* Output an HTTP response with body data, compressed as necessary.
*
* For chunked body data, an initial call with 'code' != 0 will output
* a response header and the first body chunk.
* All subsequent calls should have 'code' = 0 to output just the body chunk.
* A final call with 'len' = 0 ends the chunked body.
*
* NOTE: HTTP/1.0 clients can't handle chunked encoding,
* so we use bare chunks and close the connection when done.
*/
EXPORTED void write_body(long code, struct transaction_t *txn,
const char *buf, unsigned len)
{
unsigned is_dynamic = code ? (txn->flags.te & TE_CHUNKED) : 1;
unsigned outlen = len, offset = 0;
int do_md5 = config_getswitch(IMAPOPT_HTTPCONTENTMD5);
static MD5_CTX ctx;
static unsigned char md5[MD5_DIGEST_LENGTH];
if (!is_dynamic && len < GZIP_MIN_LEN) {
/* Don't compress small static content */
txn->resp_body.enc = CE_IDENTITY;
txn->flags.te = TE_NONE;
}
/* Compress data */
if (txn->resp_body.enc || txn->flags.te & ~TE_CHUNKED) {
#ifdef HAVE_ZLIB
/* Only flush for static content or on last (zero-length) chunk */
unsigned flush = (is_dynamic && len) ? Z_NO_FLUSH : Z_FINISH;
if (code) deflateReset(&txn->zstrm);
txn->zstrm.next_in = (Bytef *) buf;
txn->zstrm.avail_in = len;
buf_reset(&txn->zbuf);
do {
buf_ensure(&txn->zbuf,
deflateBound(&txn->zstrm, txn->zstrm.avail_in));
txn->zstrm.next_out = (Bytef *) txn->zbuf.s + txn->zbuf.len;
txn->zstrm.avail_out = txn->zbuf.alloc - txn->zbuf.len;
deflate(&txn->zstrm, flush);
txn->zbuf.len = txn->zbuf.alloc - txn->zstrm.avail_out;
} while (!txn->zstrm.avail_out);
buf = txn->zbuf.s;
outlen = txn->zbuf.len;
#else
/* XXX should never get here */
fatal("Compression requested, but no zlib", EC_SOFTWARE);
#endif /* HAVE_ZLIB */
}
if (code) {
/* Initial call - prepare response header based on CE, TE and version */
if (do_md5) MD5Init(&ctx);
if (txn->flags.te & ~TE_CHUNKED) {
/* Transfer-Encoded content MUST be chunked */
txn->flags.te |= TE_CHUNKED;
if (!is_dynamic) {
/* Handle static content as last chunk */
len = 0;
}
}
if (!(txn->flags.te & TE_CHUNKED)) {
/* Full/partial body (no encoding).
*
* In all cases, 'resp_body.len' is used to specify complete-length
* In the case of a 206 or 416 response, Content-Length will be
* set accordingly in response_header().
*/
txn->resp_body.len = outlen;
if (code == HTTP_PARTIAL) {
/* check_precond() tells us that this is a range request */
code = parse_ranges(*spool_getheader(txn->req_hdrs, "Range"),
outlen, &txn->resp_body.range);
switch (code) {
case HTTP_OK:
/* Full body (unknown range-unit) */
break;
case HTTP_PARTIAL:
/* One or more range request(s) */
txn->resp_body.len = outlen;
if (txn->resp_body.range->next) {
/* Multiple ranges */
multipart_byteranges(txn, buf);
return;
}
else {
/* Single range - set data parameters accordingly */
offset += txn->resp_body.range->first;
outlen = txn->resp_body.range->last -
txn->resp_body.range->first + 1;
}
break;
case HTTP_BAD_RANGE:
/* No valid ranges */
outlen = 0;
break;
}
}
if (outlen && do_md5) {
MD5Update(&ctx, buf+offset, outlen);
MD5Final(md5, &ctx);
txn->resp_body.md5 = md5;
}
}
else if (txn->flags.ver1_0) {
/* HTTP/1.0 doesn't support chunked - close-delimit the body */
txn->flags.conn = CONN_CLOSE;
}
else if (do_md5) txn->flags.trailer = TRAILER_CMD5;
response_header(code, txn);
/* MUST NOT send a body for 1xx/204/304 response or any HEAD response */
switch (code) {
case HTTP_CONTINUE:
case HTTP_SWITCH_PROT:
case HTTP_PROCESSING:
case HTTP_NO_CONTENT:
case HTTP_NOT_MODIFIED:
return;
default:
if (txn->meth == METH_HEAD) return;
}
}
/* Output data */
if ((txn->flags.te & TE_CHUNKED) && !txn->flags.ver1_0) {
/* HTTP/1.1 chunk */
if (outlen) {
prot_printf(httpd_out, "%x\r\n", outlen);
prot_write(httpd_out, buf, outlen);
prot_puts(httpd_out, "\r\n");
if (do_md5) MD5Update(&ctx, buf, outlen);
}
if (!len) {
/* Terminate the HTTP/1.1 body with a zero-length chunk */
prot_puts(httpd_out, "0\r\n");
/* Trailer */
if (do_md5) {
MD5Final(md5, &ctx);
Content_MD5(md5);
}
prot_puts(httpd_out, "\r\n");
}
}
else {
/* Full body or HTTP/1.0 close-delimited body */
prot_write(httpd_out, buf + offset, outlen);
}
}
/* Output an HTTP response with application/xml body */
EXPORTED void xml_response(long code, struct transaction_t *txn, xmlDocPtr xml)
{
xmlChar *buf;
int bufsiz;
switch (code) {
case HTTP_OK:
case HTTP_CREATED:
case HTTP_NO_CONTENT:
case HTTP_MULTI_STATUS:
break;
default:
/* Neither Brief nor Prefer affect error response bodies */
txn->flags.vary &= ~(VARY_BRIEF | VARY_PREFER);
txn->resp_body.prefs = 0;
}
/* Dump XML response tree into a text buffer */
xmlDocDumpFormatMemoryEnc(xml, &buf, &bufsiz, "utf-8",
config_httpprettytelemetry);
if (buf) {
/* Output the XML response */
txn->resp_body.type = "application/xml; charset=utf-8";
write_body(code, txn, (char *) buf, bufsiz);
/* Cleanup */
xmlFree(buf);
}
else {
txn->error.precond = 0;
txn->error.desc = "Error dumping XML tree\r\n";
error_response(HTTP_SERVER_ERROR, txn);
}
}
EXPORTED void buf_printf_markup(struct buf *buf, unsigned level, const char *fmt, ...)
{
va_list args;
const char *eol = "\n";
if (!config_httpprettytelemetry) {
level = 0;
eol = "";
}
va_start(args, fmt);
buf_printf(buf, "%*s", level * MARKUP_INDENT, "");
buf_vprintf(buf, fmt, args);
buf_appendcstr(buf, eol);
va_end(args);
}
/* Output an HTTP error response with optional XML or HTML body */
EXPORTED void error_response(long code, struct transaction_t *txn)
{
struct buf *html = &txn->resp_body.payload;
/* Neither Brief nor Prefer affect error response bodies */
txn->flags.vary &= ~(VARY_BRIEF | VARY_PREFER);
txn->resp_body.prefs = 0;
#ifdef WITH_DAV
if (code != HTTP_UNAUTHORIZED && txn->error.precond) {
xmlNodePtr root = xml_add_error(NULL, &txn->error, NULL);
if (root) {
xml_response(code, txn, root->doc);
xmlFreeDoc(root->doc);
return;
}
}
#endif /* WITH_DAV */
if (!txn->error.desc) {
switch (code) {
/* 4xx codes */
case HTTP_BAD_REQUEST:
txn->error.desc =
"The request was not understood by this server.";
break;
case HTTP_NOT_FOUND:
txn->error.desc =
"The requested URL was not found on this server.";
break;
case HTTP_NOT_ALLOWED:
txn->error.desc =
"The requested method is not allowed for the URL.";
break;
case HTTP_GONE:
txn->error.desc =
"The requested URL has been removed from this server.";
break;
/* 5xx codes */
case HTTP_SERVER_ERROR:
txn->error.desc =
"The server encountered an internal error.";
break;
case HTTP_NOT_IMPLEMENTED:
txn->error.desc =
"The requested method is not implemented by this server.";
break;
case HTTP_UNAVAILABLE:
txn->error.desc =
"The server is unable to process the request at this time.";
break;
}
}
buf_reset(html);
if (txn->error.desc) {
const char **hdr, *host = "";
char *port = NULL;
unsigned level = 0;
if (txn->req_hdrs &&
(hdr = spool_getheader(txn->req_hdrs, "Host")) &&
hdr[0] && *hdr[0]) {
host = (char *) hdr[0];
if ((port = strchr(host, ':'))) *port++ = '\0';
}
else if (config_serverinfo != IMAP_ENUM_SERVERINFO_OFF) {
host = config_servername;
}
if (!port) port = strchr(saslprops.iplocalport, ';')+1;
buf_printf_markup(html, level, HTML_DOCTYPE);
buf_printf_markup(html, level++, "<html>");
buf_printf_markup(html, level++, "<head>");
buf_printf_markup(html, level, "<title>%s</title>",
error_message(code));
buf_printf_markup(html, --level, "</head>");
buf_printf_markup(html, level++, "<body>");
buf_printf_markup(html, level, "<h1>%s</h1>", error_message(code)+4);
buf_printf_markup(html, level, "<p>%s</p>", txn->error.desc);
buf_printf_markup(html, level, "<hr>");
buf_printf_markup(html, level,
"<address>%s Server at %s Port %s</address>",
buf_cstring(&serverinfo), host, port);
buf_printf_markup(html, --level, "</body>");
buf_printf_markup(html, --level, "</html>");
txn->resp_body.type = "text/html; charset=utf-8";
}
write_body(code, txn, buf_cstring(html), buf_len(html));
}
static int proxy_authz(const char **authzid, struct transaction_t *txn)
{
static char authzbuf[MAX_MAILBOX_BUFFER];
unsigned authzlen;
int status;
syslog(LOG_DEBUG, "proxy_auth: authzid='%s'", *authzid);
/* Free userid & authstate previously allocated for auth'd user */
if (httpd_userid) {
free(httpd_userid);
httpd_userid = NULL;
}
if (httpd_extradomain) {
free(httpd_extradomain);
httpd_extradomain = NULL;
}
if (httpd_authstate) {
auth_freestate(httpd_authstate);
httpd_authstate = NULL;
}
if (!(config_mupdate_server && config_getstring(IMAPOPT_PROXYSERVERS))) {
/* Not a backend in a Murder - proxy authz is not allowed */
syslog(LOG_NOTICE, "badlogin: %s %s %s %s",
httpd_clienthost, txn->auth_chal.scheme->name, saslprops.authid,
"proxy authz attempted on non-Murder backend");
return SASL_NOAUTHZ;
}
/* Canonify the authzid */
status = mysasl_canon_user(httpd_saslconn, NULL,
*authzid, strlen(*authzid),
SASL_CU_AUTHZID, NULL,
authzbuf, sizeof(authzbuf), &authzlen);
if (status) {
syslog(LOG_NOTICE, "badlogin: %s %s %s invalid user",
httpd_clienthost, txn->auth_chal.scheme->name,
beautify_string(*authzid));
return status;
}
/* See if auth'd user is allowed to proxy */
status = mysasl_proxy_policy(httpd_saslconn, &httpd_proxyctx,
authzbuf, authzlen,
saslprops.authid, strlen(saslprops.authid),
NULL, 0, NULL);
if (status) {
syslog(LOG_NOTICE, "badlogin: %s %s %s %s",
httpd_clienthost, txn->auth_chal.scheme->name, saslprops.authid,
sasl_errdetail(httpd_saslconn));
return status;
}
*authzid = authzbuf;
return status;
}
/* Write cached header (redacting authorization credentials) to buffer. */
static void log_cachehdr(const char *name, const char *contents, void *rock)
{
struct buf *buf = (struct buf *) rock;
/* Ignore private headers in our cache */
if (name[0] == ':') return;
buf_printf(buf, "%c%s: ", toupper(name[0]), name+1);
if (!strcmp(name, "authorization")) {
/* Replace authorization credentials with an ellipsis */
const char *creds = strchr(contents, ' ') + 1;
buf_printf(buf, "%.*s%-*s\r\n", (int) (creds - contents), contents,
(int) strlen(creds), "...");
}
else buf_printf(buf, "%s\r\n", contents);
}
static void auth_success(struct transaction_t *txn)
{
struct auth_scheme_t *scheme = txn->auth_chal.scheme;
int i;
httpd_userisanonymous = is_userid_anonymous(httpd_userid);
proc_register("httpd", httpd_clienthost, httpd_userid, NULL, NULL);
syslog(LOG_NOTICE, "login: %s %s %s%s %s SESSIONID=<%s>",
httpd_clienthost, httpd_userid, scheme->name,
httpd_tls_done ? "+TLS" : "", "User logged in",
session_id());
/* Recreate telemetry log entry for request (w/ credentials redacted) */
assert(!buf_len(&txn->buf));
buf_printf(&txn->buf, "<%ld<", time(NULL)); /* timestamp */
buf_printf(&txn->buf, "%s %s %s\r\n", /* request-line*/
txn->req_line.meth, txn->req_line.uri, txn->req_line.ver);
spool_enum_hdrcache(txn->req_hdrs, /* header fields */
&log_cachehdr, &txn->buf);
buf_appendcstr(&txn->buf, "\r\n"); /* CRLF */
buf_append(&txn->buf, &txn->req_body.payload); /* message body */
buf_appendmap(&txn->buf, /* buffered input */
(const char *) httpd_in->ptr, httpd_in->cnt);
if (httpd_logfd != -1) {
/* Rewind log to current request and truncate it */
off_t end = lseek(httpd_logfd, 0, SEEK_END);
ftruncate(httpd_logfd, end - buf_len(&txn->buf));
}
if (!proxy_userid || strcmp(proxy_userid, httpd_userid)) {
/* Close existing telemetry log */
close(httpd_logfd);
prot_setlog(httpd_in, PROT_NO_FD);
prot_setlog(httpd_out, PROT_NO_FD);
/* Create telemetry log based on new userid */
httpd_logfd = telemetry_log(httpd_userid, httpd_in, httpd_out, 0);
}
if (httpd_logfd != -1) {
/* Log credential-redacted request */
write(httpd_logfd, buf_cstring(&txn->buf), buf_len(&txn->buf));
}
buf_reset(&txn->buf);
/* Make a copy of the external userid for use in proxying */
if (proxy_userid) free(proxy_userid);
proxy_userid = xstrdup(httpd_userid);
/* Translate any separators in userid */
mboxname_hiersep_tointernal(&httpd_namespace, httpd_userid,
config_virtdomains ?
strcspn(httpd_userid, "@") : 0);
/* Do any namespace specific post-auth processing */
for (i = 0; namespaces[i]; i++) {
if (namespaces[i]->enabled && namespaces[i]->auth)
namespaces[i]->auth(httpd_userid);
}
}
/* Perform HTTP Authentication based on the given credentials ('creds').
* Returns the selected auth scheme and any server challenge in 'chal'.
* May be called multiple times if auth scheme requires multiple steps.
* SASL status between steps is maintained in 'status'.
*/
#define BASE64_BUF_SIZE 21848 /* per RFC 4422: ((16K / 3) + 1) * 4 */
static int http_auth(const char *creds, struct transaction_t *txn)
{
struct auth_challenge_t *chal = &txn->auth_chal;
static int status = SASL_OK;
int slen;
const char *clientin = NULL, *realm = NULL, *user, **authzid;
unsigned int clientinlen = 0;
struct auth_scheme_t *scheme;
static char base64[BASE64_BUF_SIZE+1];
const void *canon_user;
/* Split credentials into auth scheme and response */
slen = strcspn(creds, " \0");
if ((clientin = strchr(creds, ' '))) clientinlen = strlen(++clientin);
syslog(LOG_DEBUG,
"http_auth: status=%d scheme='%s' creds='%.*s%s'",
status, chal->scheme ? chal->scheme->name : "",
slen, creds, clientin ? " <response>" : "");
/* Free userid & authstate previously allocated for auth'd user */
if (httpd_userid) {
free(httpd_userid);
httpd_userid = NULL;
}
if (httpd_extradomain) {
free(httpd_extradomain);
httpd_extradomain = NULL;
}
if (httpd_authstate) {
auth_freestate(httpd_authstate);
httpd_authstate = NULL;
}
chal->param = NULL;
if (chal->scheme) {
/* Use current scheme, if possible */
scheme = chal->scheme;
if (strncasecmp(scheme->name, creds, slen)) {
/* Changing auth scheme -> reset state */
syslog(LOG_DEBUG, "http_auth: changing scheme");
reset_saslconn(&httpd_saslconn);
chal->scheme = NULL;
status = SASL_OK;
}
}
if (!chal->scheme) {
/* Find the client-specified auth scheme */
syslog(LOG_DEBUG, "http_auth: find client scheme");
for (scheme = auth_schemes; scheme->name; scheme++) {
if (slen && !strncasecmp(scheme->name, creds, slen)) {
/* Found a supported scheme, see if its available */
if (!(avail_auth_schemes & (1 << scheme->idx))) scheme = NULL;
break;
}
}
if (!scheme || !scheme->name) {
/* Didn't find a matching scheme that is available */
syslog(LOG_DEBUG, "Unknown auth scheme '%.*s'", slen, creds);
return SASL_NOMECH;
}
/* We found it! */
syslog(LOG_DEBUG, "http_auth: found matching scheme: %s", scheme->name);
chal->scheme = scheme;
status = SASL_OK;
}
/* Base64 decode any client response, if necesary */
if (clientin && (scheme->flags & AUTH_BASE64)) {
int r = sasl_decode64(clientin, clientinlen,
base64, BASE64_BUF_SIZE, &clientinlen);
if (r != SASL_OK) {
syslog(LOG_ERR, "Base64 decode failed: %s",
sasl_errstring(r, NULL, NULL));
return r;
}
clientin = base64;
}
/* Get realm - based on namespace of URL */
switch (txn->req_tgt.namespace) {
case URL_NS_DEFAULT:
case URL_NS_PRINCIPAL:
realm = config_getstring(IMAPOPT_DAV_REALM);
break;
case URL_NS_CALENDAR:
realm = config_getstring(IMAPOPT_CALDAV_REALM);
break;
case URL_NS_ADDRESSBOOK:
realm = config_getstring(IMAPOPT_CARDDAV_REALM);
break;
case URL_NS_RSS:
realm = config_getstring(IMAPOPT_RSS_REALM);
break;
}
if (!realm) realm = config_servername;
#ifdef SASL_HTTP_REQUEST
/* Setup SASL HTTP request, if necessary */
if (scheme->flags & AUTH_NEED_REQUEST) {
sasl_http_request_t sasl_http_req;
sasl_http_req.method = txn->req_line.meth;
sasl_http_req.uri = txn->req_line.uri;
sasl_http_req.entity = NULL;
sasl_http_req.elen = 0;
sasl_http_req.non_persist = txn->flags.conn & CONN_CLOSE;
sasl_setprop(httpd_saslconn, SASL_HTTP_REQUEST, &sasl_http_req);
}
#endif /* SASL_HTTP_REQUEST */
if (scheme->idx == AUTH_BASIC) {
/* Basic (plaintext) authentication */
char *pass;
char *extra;
if (!clientin) {
/* Create initial challenge (base64 buffer is static) */
snprintf(base64, BASE64_BUF_SIZE, "realm=\"%s\"", realm);
chal->param = base64;
chal->scheme = NULL; /* make sure we don't reset the SASL ctx */
return status;
}
/* Split credentials into <user> ':' <pass>.
* We are working with base64 buffer, so we can modify it.
*/
user = base64;
pass = strchr(base64, ':');
if (!pass) {
syslog(LOG_ERR, "Basic auth: Missing password");
return SASL_BADPARAM;
}
*pass++ = '\0';
extra = strchr(user, '%');
if (extra) *extra++ = '\0';
/* Verify the password */
status = sasl_checkpass(httpd_saslconn, user, strlen(user),
pass, strlen(pass));
memset(pass, 0, strlen(pass)); /* erase plaintext password */
if (status) {
syslog(LOG_NOTICE, "badlogin: %s Basic %s %s",
httpd_clienthost, user, sasl_errdetail(httpd_saslconn));
/* Don't allow user probing */
if (status == SASL_NOUSER) status = SASL_BADAUTH;
return status;
}
/* Successful authentication - fall through */
httpd_extradomain = xstrdupnull(extra);
}
else {
/* SASL-based authentication (Digest, Negotiate, NTLM) */
const char *serverout = NULL;
unsigned int serveroutlen = 0;
if (status == SASL_CONTINUE) {
/* Continue current authentication exchange */
syslog(LOG_DEBUG, "http_auth: continue %s", scheme->saslmech);
status = sasl_server_step(httpd_saslconn, clientin, clientinlen,
&serverout, &serveroutlen);
}
else {
/* Start new authentication exchange */
syslog(LOG_DEBUG, "http_auth: start %s", scheme->saslmech);
status = sasl_server_start(httpd_saslconn, scheme->saslmech,
clientin, clientinlen,
&serverout, &serveroutlen);
}
/* Failure - probably bad client response */
if ((status != SASL_OK) && (status != SASL_CONTINUE)) {
syslog(LOG_ERR, "SASL failed: %s",
sasl_errstring(status, NULL, NULL));
return status;
}
/* Base64 encode any server challenge, if necesary */
if (serverout && (scheme->flags & AUTH_BASE64)) {
int r = sasl_encode64(serverout, serveroutlen,
base64, BASE64_BUF_SIZE, NULL);
if (r != SASL_OK) {
syslog(LOG_ERR, "Base64 encode failed: %s",
sasl_errstring(r, NULL, NULL));
return r;
}
serverout = base64;
}
chal->param = serverout;
if (status == SASL_CONTINUE) {
/* Need another step to complete authentication */
return status;
}
/* Successful authentication
*
* HTTP doesn't support security layers,
* so don't attach SASL context to prot layer.
*/
}
/* Get the userid from SASL - already canonicalized */
status = sasl_getprop(httpd_saslconn, SASL_USERNAME, &canon_user);
if (status != SASL_OK) {
syslog(LOG_ERR, "weird SASL error %d getting SASL_USERNAME", status);
return status;
}
user = (const char *) canon_user;
if (saslprops.authid) free(saslprops.authid);
saslprops.authid = xstrdup(user);
authzid = spool_getheader(txn->req_hdrs, "Authorize-As");
if (authzid && *authzid[0]) {
/* Trying to proxy as another user */
user = authzid[0];
status = proxy_authz(&user, txn);
if (status) return status;
}
httpd_userid = xstrdup(user);
auth_success(txn);
return status;
}
/************************* Method Execution Routines ************************/
/* Compare an etag in a header to a resource etag.
* Returns 0 if a match, non-zero otherwise.
*/
EXPORTED int etagcmp(const char *hdr, const char *etag)
{
size_t len;
if (!etag) return -1; /* no representation */
if (!strcmp(hdr, "*")) return 0; /* any representation */
len = strlen(etag);
if (!strncmp(hdr, "W/", 2)) hdr+=2; /* skip weak prefix */
if (*hdr++ != '\"') return 1; /* match/skip open DQUOTE */
if (strlen(hdr) != len+1) return 1; /* make sure lengths match */
if (hdr[len] != '\"') return 1; /* match close DQUOTE */
return strncmp(hdr, etag, len);
}
/* Compare a resource etag to a comma-separated list and/or multiple headers
* looking for a match. Returns 1 if a match is found, 0 otherwise.
*/
static unsigned etag_match(const char *hdr[], const char *etag)
{
unsigned i, match = 0;
tok_t tok;
char *token;
for (i = 0; !match && hdr[i]; i++) {
tok_init(&tok, hdr[i], ",", TOK_TRIMLEFT|TOK_TRIMRIGHT);
while (!match && (token = tok_next(&tok))) {
if (!etagcmp(token, etag)) match = 1;
}
tok_fini(&tok);
}
return match;
}
static int parse_ranges(const char *hdr, unsigned long len,
struct range **ranges)
{
int ret = HTTP_BAD_RANGE;
struct range *new, *tail = *ranges = NULL;
tok_t tok;
char *token;
if (!len) return HTTP_OK; /* need to know length of representation */
/* we only handle byte-unit */
if (!hdr || strncmp(hdr, "bytes=", 6)) return HTTP_OK;
tok_init(&tok, hdr+6, ",", TOK_TRIMLEFT|TOK_TRIMRIGHT);
while ((token = tok_next(&tok))) {
/* default to entire representation */
unsigned long first = 0;
unsigned long last = len - 1;
char *p, *endp;
if (!(p = strchr(token, '-'))) continue; /* bad byte-range-set */
if (p == token) {
/* suffix-byte-range-spec */
unsigned long suffix = strtoul(++p, &endp, 10);
if (endp == p || *endp) continue; /* bad suffix-length */
if (!suffix) continue; /* unsatisfiable suffix-length */
/* don't start before byte zero */
if (suffix < len) first = len - suffix;
}
else {
/* byte-range-spec */
first = strtoul(token, &endp, 10);
if (endp != p) continue; /* bad first-byte-pos */
if (first >= len) continue; /* unsatisfiable first-byte-pos */
if (*++p) {
/* last-byte-pos */
last = strtoul(p, &endp, 10);
if (*endp || last < first) continue; /* bad last-byte-pos */
/* don't go past end of representation */
if (last >= len) last = len - 1;
}
}
ret = HTTP_PARTIAL;
/* Coalesce overlapping ranges, or those with a gap < 80 bytes */
if (tail &&
first >= tail->first && (long) (first - tail->last) < 80) {
tail->last = MAX(last, tail->last);
continue;
}
/* Create a new range and append it to linked list */
new = xzmalloc(sizeof(struct range));
new->first = first;
new->last = last;
if (tail) tail->next = new;
else *ranges = new;
tail = new;
}
tok_fini(&tok);
return ret;
}
/* Check headers for any preconditions.
*
* Interaction is complex and is documented in RFC 7232
*/
EXPORTED int check_precond(struct transaction_t *txn,
const char *etag, time_t lastmod)
{
hdrcache_t hdrcache = txn->req_hdrs;
const char **hdr;
time_t since;
/* Step 1 */
if ((hdr = spool_getheader(hdrcache, "If-Match"))) {
if (!etag_match(hdr, etag)) return HTTP_PRECOND_FAILED;
/* Continue to step 3 */
}
/* Step 2 */
else if ((hdr = spool_getheader(hdrcache, "If-Unmodified-Since"))) {
if (time_from_rfc822(hdr[0], &since))
return HTTP_BAD_REQUEST;
if (since && (lastmod > since)) return HTTP_PRECOND_FAILED;
/* Continue to step 3 */
}
/* Step 3 */
if ((hdr = spool_getheader(hdrcache, "If-None-Match"))) {
if (etag_match(hdr, etag)) {
if (txn->meth == METH_GET || txn->meth == METH_HEAD)
return HTTP_NOT_MODIFIED;
else
return HTTP_PRECOND_FAILED;
}
/* Continue to step 5 */
}
/* Step 4 */
else if ((txn->meth == METH_GET || txn->meth == METH_HEAD) &&
(hdr = spool_getheader(hdrcache, "If-Modified-Since"))) {
if (time_from_rfc822(hdr[0], &since))
return HTTP_BAD_REQUEST;
if (lastmod <= since) return HTTP_NOT_MODIFIED;
/* Continue to step 5 */
}
/* Step 5 */
if (txn->flags.ranges && /* Only if we support Range requests */
txn->meth == METH_GET && (hdr = spool_getheader(hdrcache, "Range"))) {
if ((hdr = spool_getheader(hdrcache, "If-Range"))) {
time_from_rfc822(hdr[0], &since); /* error OK here, could be an etag */
}
/* Only process Range if If-Range isn't present or validator matches */
if (!hdr || (since && (lastmod <= since)) || !etagcmp(hdr[0], etag))
return HTTP_PARTIAL;
}
/* Step 6 */
return HTTP_OK;
}
const struct mimetype {
const char *ext;
const char *type;
unsigned int compressible;
} mimetypes[] = {
{ ".css", "text/css", 1 },
{ ".htm", "text/html", 1 },
{ ".html", "text/html", 1 },
{ ".ics", "text/calendar", 1 },
{ ".ifb", "text/calendar", 1 },
{ ".text", "text/plain", 1 },
{ ".txt", "text/plain", 1 },
{ ".cgm", "image/cgm", 1 },
{ ".gif", "image/gif", 0 },
{ ".jpg", "image/jpeg", 0 },
{ ".jpeg", "image/jpeg", 0 },
{ ".png", "image/png", 0 },
{ ".svg", "image/svg+xml", 1 },
{ ".tif", "image/tiff", 1 },
{ ".tiff", "image/tiff", 1 },
{ ".aac", "audio/aac", 0 },
{ ".m4a", "audio/mp4", 0 },
{ ".mp3", "audio/mpeg", 0 },
{ ".mpeg", "audio/mpeg", 0 },
{ ".oga", "audio/ogg", 0 },
{ ".ogg", "audio/ogg", 0 },
{ ".wav", "audio/wav", 0 },
{ ".avi", "video/x-msvideo", 0 },
{ ".mov", "video/quicktime", 0 },
{ ".m4v", "video/mp4", 0 },
{ ".ogv", "video/ogg", 0 },
{ ".qt", "video/quicktime", 0 },
{ ".wmv", "video/x-ms-wmv", 0 },
{ ".bz", "application/x-bzip", 0 },
{ ".bz2", "application/x-bzip2", 0 },
{ ".gz", "application/gzip", 0 },
{ ".gzip", "application/gzip", 0 },
{ ".tgz", "application/gzip", 0 },
{ ".zip", "application/zip", 0 },
{ ".doc", "application/msword", 1 },
{ ".jcs", "application/calendar+json", 1 },
{ ".jfb", "application/calendar+json", 1 },
{ ".js", "application/javascript", 1 },
{ ".json", "application/json", 1 },
{ ".pdf", "application/pdf", 1 },
{ ".ppt", "application/vnd.ms-powerpoint", 1 },
{ ".sh", "application/x-sh", 1 },
{ ".tar", "application/x-tar", 1 },
{ ".xcs", "application/calendar+xml", 1 },
{ ".xfb", "application/calendar+xml", 1 },
{ ".xls", "application/vnd.ms-excel", 1 },
{ ".xml", "application/xml", 1 },
{ NULL, NULL, 0 }
};
static int list_well_known(struct transaction_t *txn)
{
static struct buf body = BUF_INITIALIZER;
static time_t lastmod = 0;
struct stat sbuf;
int precond;
/* stat() imapd.conf for Last-Modified and ETag */
stat(config_filename, &sbuf);
assert(!buf_len(&txn->buf));
buf_printf(&txn->buf, "%ld-%ld-%ld",
compile_time, sbuf.st_mtime, sbuf.st_size);
sbuf.st_mtime = MAX(compile_time, sbuf.st_mtime);
/* 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_NOT_MODIFIED:
/* Fill in ETag, Last-Modified, and Expires */
txn->resp_body.etag = buf_cstring(&txn->buf);
txn->resp_body.lastmod = sbuf.st_mtime;
txn->resp_body.maxage = 86400; /* 24 hrs */
txn->flags.cc |= CC_MAXAGE;
if (precond != HTTP_NOT_MODIFIED) break;
default:
/* We failed a precondition - don't perform the request */
return precond;
}
if (txn->resp_body.lastmod > lastmod) {
const char *proto = NULL, *host = NULL;
unsigned i, level = 0;
/* Start HTML */
buf_reset(&body);
buf_printf_markup(&body, level, HTML_DOCTYPE);
buf_printf_markup(&body, level++, "<html>");
buf_printf_markup(&body, level++, "<head>");
buf_printf_markup(&body, level,
"<title>%s</title>", "Well-Known Locations");
buf_printf_markup(&body, --level, "</head>");
buf_printf_markup(&body, level++, "<body>");
buf_printf_markup(&body, level,
"<h2>%s</h2>", "Well-Known Locations");
buf_printf_markup(&body, level++, "<ul>");
/* Add the list of enabled /.well-known/ URLs */
http_proto_host(txn->req_hdrs, &proto, &host);
for (i = 0; namespaces[i]; i++) {
if (namespaces[i]->enabled && namespaces[i]->well_known) {
buf_printf_markup(&body, level,
"<li><a href=\"%s://%s%s\">%s</a></li>",
proto, host, namespaces[i]->prefix,
namespaces[i]->well_known);
}
}
/* Finish HTML */
buf_printf_markup(&body, --level, "</ul>");
buf_printf_markup(&body, --level, "</body>");
buf_printf_markup(&body, --level, "</html>");
lastmod = txn->resp_body.lastmod;
}
/* Output the HTML response */
txn->resp_body.type = "text/html; charset=utf-8";
write_body(precond, txn, buf_cstring(&body), buf_len(&body));
return 0;
}
#define WELL_KNOWN_PREFIX "/.well-known"
/* Perform a GET/HEAD request */
static int meth_get(struct transaction_t *txn,
void *params __attribute__((unused)))
{
int ret = 0, r, fd = -1, precond, len;
const char *prefix, *urls, *path, *ext;
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;
/* Check if this is a request for /.well-known/ listing */
len = strlen(WELL_KNOWN_PREFIX);
if (!strncmp(txn->req_uri->path, WELL_KNOWN_PREFIX, len)) {
if (txn->req_uri->path[len] == '/') len++;
if (txn->req_uri->path[len] == '\0') return list_well_known(txn);
else return HTTP_NOT_FOUND;
}
/* Serve up static pages */
prefix = config_getstring(IMAPOPT_HTTPDOCROOT);
if (!prefix) return HTTP_NOT_FOUND;
if (*prefix != '/') {
/* Remote content */
struct backend *be;
be = proxy_findserver(prefix, &http_protocol, proxy_userid,
&backend_cached, NULL, NULL, httpd_in);
if (!be) return HTTP_UNAVAILABLE;
return http_pipe_req_resp(be, txn);
}
/* Local content */
if ((urls = config_getstring(IMAPOPT_HTTPALLOWEDURLS))) {
tok_t tok = TOK_INITIALIZER(urls, " \t", TOK_TRIMLEFT|TOK_TRIMRIGHT);
char *token;
while ((token = tok_next(&tok)) && strcmp(token, txn->req_uri->path))
tok_fini(&tok);
if (!token) return HTTP_NOT_FOUND;
}
buf_setcstr(&pathbuf, prefix);
buf_appendcstr(&pathbuf, txn->req_uri->path);
path = buf_cstring(&pathbuf);
/* See if path is a directory and look for index.html */
if (!(r = stat(path, &sbuf)) && S_ISDIR(sbuf.st_mode)) {
buf_appendcstr(&pathbuf, "/index.html");
path = buf_cstring(&pathbuf);
r = stat(path, &sbuf);
}
/* See if file exists and get Content-Length & Last-Modified time */
if (r || !S_ISREG(sbuf.st_mode)) return HTTP_NOT_FOUND;
if (!resp_body->type) {
/* Caller hasn't specified the Content-Type */
resp_body->type = "application/octet-stream";
if ((ext = strrchr(path, '.'))) {
/* Try to use filename extension to identity Content-Type */
const struct mimetype *mtype;
for (mtype = mimetypes; mtype->ext; mtype++) {
if (!strcasecmp(ext, mtype->ext)) {
resp_body->type = mtype->type;
if (!mtype->compressible) {
/* Never compress non-compressible resources */
txn->resp_body.enc = CE_IDENTITY;
txn->flags.te = TE_NONE;
txn->flags.vary &= ~VARY_AE;
}
break;
}
}
}
}
/* 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 ETag, Last-Modified, and Expires */
resp_body->etag = buf_cstring(&txn->buf);
resp_body->lastmod = sbuf.st_mtime;
resp_body->maxage = 86400; /* 24 hrs */
txn->flags.cc |= CC_MAXAGE;
if (!httpd_userisanonymous) txn->flags.cc |= CC_PUBLIC;
if (precond != HTTP_NOT_MODIFIED) break;
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;
}
/* Perform an OPTIONS request */
EXPORTED int meth_options(struct transaction_t *txn, void *params)
{
parse_path_t parse_path = (parse_path_t) params;
int r, i;
/* Response should not be cached */
txn->flags.cc |= CC_NOCACHE;
/* Response doesn't have a body, so no Vary */
txn->flags.vary = 0;
/* Special case "*" - show all features/methods available on server */
if (!strcmp(txn->req_uri->path, "*")) {
for (i = 0; namespaces[i]; i++) {
if (namespaces[i]->enabled)
txn->req_tgt.allow |= namespaces[i]->allow;
}
}
else {
if (parse_path) {
/* Parse the path */
r = parse_path(txn->req_uri->path, &txn->req_tgt, &txn->error.desc);
if (r) return r;
}
if (txn->flags.cors) {
const char **hdr =
spool_getheader(txn->req_hdrs, "Access-Control-Request-Method");
if (hdr) {
/* CORS preflight request */
unsigned meth;
txn->flags.cors = CORS_PREFLIGHT;
/* Check Method against our list of known methods */
for (meth = 0; (meth < METH_UNKNOWN) &&
strcmp(http_methods[meth].name, hdr[0]); meth++);
if (meth == METH_UNKNOWN) txn->flags.cors = 0;
else {
/* Check Method against those supported by the resource */
for (i = 0; namespaces[i] &&
namespaces[i]->id != txn->req_tgt.namespace; i++);
if (!namespaces[i]->methods[meth].proc) txn->flags.cors = 0;
}
}
}
}
response_header(HTTP_OK, txn);
return 0;
}
/* Perform an PROPFIND request on "/" iff we support CalDAV */
static int meth_propfind_root(struct transaction_t *txn,
void *params __attribute__((unused)))
{
assert(txn);
#ifdef WITH_DAV
/* Apple iCal and Evolution both check "/" */
if (!strcmp(txn->req_uri->path, "/") ||
!strcmp(txn->req_uri->path, "/dav/")) {
/* Array of known "live" properties */
const struct prop_entry root_props[] = {
/* WebDAV ACL (RFC 3744) properties */
{ "principal-collection-set", NS_DAV, PROP_COLLECTION,
propfind_princolset, NULL, NULL },
/* WebDAV Current Principal (RFC 5397) properties */
{ "current-user-principal", NS_DAV, PROP_COLLECTION,
propfind_curprin, NULL, NULL },
{ NULL, 0, 0, NULL, NULL, NULL }
};
struct meth_params root_params = {
.lprops = root_props
};
/* Make a working copy of target path */
strlcpy(txn->req_tgt.path, txn->req_uri->path,
sizeof(txn->req_tgt.path));
txn->req_tgt.tail = txn->req_tgt.path + strlen(txn->req_tgt.path);
txn->req_tgt.allow |= ALLOW_DAV;
return meth_propfind(txn, &root_params);
}
#endif /* WITH_DAV */
return HTTP_NOT_ALLOWED;
}
/* Write cached header to buf, excluding any that might have sensitive data. */
static void trace_cachehdr(const char *name, const char *contents, void *rock)
{
struct buf *buf = (struct buf *) rock;
const char **hdr, *sensitive[] =
{ "authorization", "cookie", "proxy-authorization", NULL };
/* Ignore private headers in our cache */
if (name[0] == ':') return;
for (hdr = sensitive; *hdr && strcmp(name, *hdr); hdr++);
if (!*hdr) buf_printf(buf, "%c%s: %s\r\n",
toupper(name[0]), name+1, contents);
}
/* Perform an TRACE request */
EXPORTED int meth_trace(struct transaction_t *txn, void *params)
{
parse_path_t parse_path = (parse_path_t) params;
const char **hdr;
unsigned long max_fwd = -1;
struct buf *msg = &txn->resp_body.payload;
/* Response should not be cached */
txn->flags.cc |= CC_NOCACHE;
/* Make sure method is allowed */
if (!(txn->req_tgt.allow & ALLOW_TRACE)) return HTTP_NOT_ALLOWED;
if ((hdr = spool_getheader(txn->req_hdrs, "Max-Forwards"))) {
max_fwd = strtoul(hdr[0], NULL, 10);
}
if (max_fwd && parse_path) {
/* Parse the path */
int r;
if ((r = parse_path(txn->req_uri->path,
&txn->req_tgt, &txn->error.desc))) return r;
if (txn->req_tgt.mbentry && txn->req_tgt.mbentry->server) {
/* Remote mailbox */
struct backend *be;
be = proxy_findserver(txn->req_tgt.mbentry->server,
&http_protocol, proxy_userid,
&backend_cached, NULL, NULL, httpd_in);
if (!be) return HTTP_UNAVAILABLE;
return http_pipe_req_resp(be, txn);
}
/* Local mailbox */
}
/* Echo the request back to the client as a message/http:
*
* - Piece the Request-line back together
* - Use all non-sensitive cached headers from client
*/
buf_reset(msg);
buf_printf(msg, "TRACE %s %s\r\n", txn->req_line.uri, txn->req_line.ver);
spool_enum_hdrcache(txn->req_hdrs, &trace_cachehdr, msg);
buf_appendcstr(msg, "\r\n");
txn->resp_body.type = "message/http";
txn->resp_body.len = buf_len(msg);
write_body(HTTP_OK, txn, buf_cstring(msg), buf_len(msg));
return 0;
}
/* simple wrapper to implicity add READFB if we have the READ ACL */
EXPORTED int httpd_myrights(struct auth_state *authstate, const char *acl)
{
int rights = acl ? cyrus_acl_myrights(authstate, acl) : 0;
if ((rights & DACL_READ) == DACL_READ) rights |= DACL_READFB;
return rights;
}

File Metadata

Mime Type
text/x-c
Expires
Sat, Apr 4, 1:40 AM (1 w, 5 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18821968
Default Alt Text
httpd.c (101 KB)

Event Timeline