apreq_cookie.c revision 6ac28b74a504006c016d4df3fb84d2cd2e373942
/*
** Licensed to the Apache Software Foundation (ASF) under one or more
** contributor license agreements. See the NOTICE file distributed with
** this work for additional information regarding copyright ownership.
** The ASF licenses this file to You under the Apache License, Version 2.0
** (the "License"); you may not use this file except in compliance with
** the License. You may obtain a copy of the License at
**
** http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an "AS IS" BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
*/
#include "apreq_cookie.h"
#include "apreq_error.h"
#include "apreq_util.h"
#include "apr_strings.h"
#include "apr_lib.h"
#include "apr_date.h"
#define RFC 1
#define NETSCAPE 0
#define ADD_COOKIE(j,c) apreq_value_table_add(&c->v, j)
APREQ_DECLARE(void) apreq_cookie_expires(apreq_cookie_t *c,
const char *time_str)
{
if (time_str == NULL) {
c->max_age = -1;
return;
}
if (!strcasecmp(time_str, "now"))
c->max_age = 0;
else {
c->max_age = apr_date_parse_rfc(time_str);
if (c->max_age == APR_DATE_BAD)
c->max_age = apr_time_from_sec(apreq_atoi64t(time_str));
else
c->max_age -= apr_time_now();
}
}
static apr_status_t apreq_cookie_attr(apr_pool_t *p,
apreq_cookie_t *c,
const char *attr,
apr_size_t alen,
const char *val,
apr_size_t vlen)
{
if (alen < 2)
return APR_EBADARG;
if ( attr[0] == '-' || attr[0] == '$' ) {
++attr;
--alen;
}
switch (apr_tolower(*attr)) {
case 'n': /* name is not an attr */
return APR_ENOTIMPL;
case 'v': /* version; value is not an attr */
if (alen == 5 && strncasecmp(attr,"value", 5) == 0)
return APR_ENOTIMPL;
while (!apr_isdigit(*val)) {
if (vlen == 0)
return APREQ_ERROR_BADSEQ;
++val;
--vlen;
}
apreq_cookie_version_set(c, *val - '0');
return APR_SUCCESS;
case 'e': case 'm': /* expires, max-age */
apreq_cookie_expires(c, val);
return APR_SUCCESS;
case 'd':
c->domain = apr_pstrmemdup(p,val,vlen);
return APR_SUCCESS;
case 'p':
if (alen != 4)
break;
if (!strncasecmp("port", attr, 4)) {
c->port = apr_pstrmemdup(p,val,vlen);
return APR_SUCCESS;
}
else if (!strncasecmp("path", attr, 4)) {
c->path = apr_pstrmemdup(p,val,vlen);
return APR_SUCCESS;
}
break;
case 'c':
if (!strncasecmp("commentURL", attr, 10)) {
c->commentURL = apr_pstrmemdup(p,val,vlen);
return APR_SUCCESS;
}
else if (!strncasecmp("comment", attr, 7)) {
c->comment = apr_pstrmemdup(p,val,vlen);
return APR_SUCCESS;
}
break;
case 's':
if (vlen > 0 && *val != '0' && strncasecmp("off",val,vlen))
apreq_cookie_secure_on(c);
else
apreq_cookie_secure_off(c);
return APR_SUCCESS;
case 'h': /* httponly */
if (vlen > 0 && *val != '0' && strncasecmp("off",val,vlen))
apreq_cookie_httponly_on(c);
else
apreq_cookie_httponly_off(c);
return APR_SUCCESS;
};
return APR_ENOTIMPL;
}
APREQ_DECLARE(apreq_cookie_t *) apreq_cookie_make(apr_pool_t *p,
const char *name,
const apr_size_t nlen,
const char *value,
const apr_size_t vlen)
{
apreq_cookie_t *c;
apreq_value_t *v;
c = apr_palloc(p, nlen + vlen + 1 + sizeof *c);
if (c == NULL)
return NULL;
*(const apreq_value_t **)&v = &c->v;
if (vlen > 0 && value != NULL)
memcpy(v->data, value, vlen);
v->data[vlen] = 0;
v->dlen = vlen;
v->name = v->data + vlen + 1;
if (nlen && name != NULL)
memcpy(v->name, name, nlen);
v->name[nlen] = 0;
v->nlen = nlen;
c->path = NULL;
c->domain = NULL;
c->port = NULL;
c->comment = NULL;
c->commentURL = NULL;
c->max_age = -1; /* session cookie is the default */
c->flags = 0;
return c;
}
static APR_INLINE
apr_status_t get_pair(apr_pool_t *p, const char **data,
const char **n, apr_size_t *nlen,
const char **v, apr_size_t *vlen, unsigned unquote)
{
const char *hdr, *key, *val;
int nlen_set = 0;
hdr = *data;
while (apr_isspace(*hdr) || *hdr == '=')
++hdr;
key = hdr;
*n = hdr;
scan_name:
switch (*hdr) {
case 0:
case ';':
case ',':
if (!nlen_set)
*nlen = hdr - key;
*v = hdr;
*vlen = 0;
*data = hdr;
return *nlen ? APREQ_ERROR_NOTOKEN : APREQ_ERROR_BADCHAR;
case '=':
if (!nlen_set) {
*nlen = hdr - key;
nlen_set = 1;
}
break;
case ' ':
case '\t':
case '\r':
case '\n':
if (!nlen_set) {
*nlen = hdr - key;
nlen_set = 1;
}
/* fall thru */
default:
++hdr;
goto scan_name;
}
val = hdr + 1;
while (apr_isspace(*val))
++val;
if (*val == '"') {
unsigned saw_backslash = 0;
for (*v = (unquote) ? ++val : val++; *val; ++val) {
switch (*val) {
case '"':
*data = val + 1;
if (!unquote) {
*vlen = (val - *v) + 1;
}
else if (!saw_backslash) {
*vlen = val - *v;
}
else {
char *dest = apr_palloc(p, val - *v), *d = dest;
const char *s = *v;
while (s < val) {
if (*s == '\\')
++s;
*d++ = *s++;
}
*vlen = d - dest;
*v = dest;
}
return APR_SUCCESS;
case '\\':
saw_backslash = 1;
if (val[1] != 0)
++val;
default:
break;
}
}
/* bad sequence: no terminating quote found */
*data = val;
return APREQ_ERROR_BADSEQ;
}
else {
/* value is not wrapped in quotes */
for (*v = val; *val; ++val) {
switch (*val) {
case ';':
case ',':
case ' ':
case '\t':
case '\r':
case '\n':
*data = val;
*vlen = val - *v;
return APR_SUCCESS;
default:
break;
}
}
}
*data = val;
*vlen = val - *v;
return APR_SUCCESS;
}
APREQ_DECLARE(apr_status_t)apreq_parse_cookie_header(apr_pool_t *p,
apr_table_t *j,
const char *hdr)
{
apreq_cookie_t *c;
unsigned version;
apr_status_t rv = APR_SUCCESS;
parse_cookie_header:
c = NULL;
version = NETSCAPE;
while (apr_isspace(*hdr))
++hdr;
if (*hdr == '$' && strncasecmp(hdr, "$Version", 8) == 0) {
/* XXX cheat: assume "$Version" => RFC Cookie header */
version = RFC;
skip_version_string:
switch (*hdr++) {
case 0:
return rv;
case ',':
goto parse_cookie_header;
case ';':
break;
default:
goto skip_version_string;
}
}
for (;;) {
apr_status_t status;
const char *name, *value;
apr_size_t nlen, vlen;
while (*hdr == ';' || apr_isspace(*hdr))
++hdr;
switch (*hdr) {
case 0:
/* this is the normal exit point */
if (c != NULL) {
ADD_COOKIE(j, c);
}
return rv;
case ',':
++hdr;
if (c != NULL) {
ADD_COOKIE(j, c);
}
goto parse_cookie_header;
case '$':
++hdr;
if (c == NULL) {
rv = APREQ_ERROR_BADCHAR;
goto parse_cookie_error;
}
else if (version == NETSCAPE) {
rv = APREQ_ERROR_MISMATCH;
}
status = get_pair(p, &hdr, &name, &nlen, &value, &vlen, 1);
if (status != APR_SUCCESS) {
rv = status;
goto parse_cookie_error;
}
status = apreq_cookie_attr(p, c, name, nlen, value, vlen);
switch (status) {
case APR_ENOTIMPL:
rv = APREQ_ERROR_BADATTR;
/* fall thru */
case APR_SUCCESS:
break;
default:
rv = status;
goto parse_cookie_error;
}
break;
default:
if (c != NULL) {
ADD_COOKIE(j, c);
}
status = get_pair(p, &hdr, &name, &nlen, &value, &vlen, 0);
if (status != APR_SUCCESS) {
c = NULL;
rv = status;
goto parse_cookie_error;
}
c = apreq_cookie_make(p, name, nlen, value, vlen);
apreq_cookie_tainted_on(c);
if (version != NETSCAPE)
apreq_cookie_version_set(c, version);
}
}
parse_cookie_error:
switch (*hdr) {
case 0:
return rv;
case ',':
case ';':
if (c != NULL)
ADD_COOKIE(j, c);
++hdr;
goto parse_cookie_header;
default:
++hdr;
goto parse_cookie_error;
}
/* not reached */
return rv;
}
APREQ_DECLARE(int) apreq_cookie_serialize(const apreq_cookie_t *c,
char *buf, apr_size_t len)
{
/* The format string must be large enough to accomodate all
* of the cookie attributes. The current attributes sum to
* ~90 characters (w/ 6-8 padding chars per attr), so anything
* over 100 should be fine.
*/
unsigned version = apreq_cookie_version(c);
char format[128] = "%s=%s";
char *f = format + strlen(format);
/* XXX protocol enforcement (for debugging, anyway) ??? */
if (c->v.name == NULL)
return -1;
#define NULL2EMPTY(attr) (attr ? attr : "")
if (version == NETSCAPE) {
char expires[APR_RFC822_DATE_LEN] = {0};
#define ADD_NS_ATTR(name) do { \
if (c->name != NULL) \
strcpy(f, "; " #name "=%s"); \
else \
strcpy(f, "%0.s"); \
f += strlen(f); \
} while (0)
ADD_NS_ATTR(path);
ADD_NS_ATTR(domain);
if (c->max_age != -1) {
strcpy(f, "; expires=%s");
apr_rfc822_date(expires, c->max_age + apr_time_now());
expires[7] = '-';
expires[11] = '-';
}
else
strcpy(f, "");
f += strlen(f);
if (apreq_cookie_is_secure(c))
strcpy(f, "; secure");
f += strlen(f);
if (apreq_cookie_is_httponly(c))
strcpy(f, "; HttpOnly");
return apr_snprintf(buf, len, format, c->v.name, c->v.data,
NULL2EMPTY(c->path), NULL2EMPTY(c->domain), expires);
}
/* c->version == RFC */
strcpy(f,"; Version=%u");
f += strlen(f);
/* ensure RFC attributes are always quoted */
#define ADD_RFC_ATTR(name) do { \
if (c->name != NULL) \
if (*c->name == '"') \
strcpy(f, "; " #name "=%s"); \
else \
strcpy(f, "; " #name "=\"%s\""); \
else \
strcpy(f, "%0.s"); \
f += strlen (f); \
} while (0)
ADD_RFC_ATTR(path);
ADD_RFC_ATTR(domain);
ADD_RFC_ATTR(port);
ADD_RFC_ATTR(comment);
ADD_RFC_ATTR(commentURL);
strcpy(f, c->max_age != -1 ? "; max-age=%" APR_TIME_T_FMT : "");
f += strlen(f);
if (apreq_cookie_is_secure(c))
strcpy(f, "; secure");
f += strlen(f);
if (apreq_cookie_is_httponly(c))
strcpy(f, "; HttpOnly");
return apr_snprintf(buf, len, format, c->v.name, c->v.data, version,
NULL2EMPTY(c->path), NULL2EMPTY(c->domain),
NULL2EMPTY(c->port), NULL2EMPTY(c->comment),
NULL2EMPTY(c->commentURL), apr_time_sec(c->max_age));
}
APREQ_DECLARE(char*) apreq_cookie_as_string(const apreq_cookie_t *c,
apr_pool_t *p)
{
int n = apreq_cookie_serialize(c, NULL, 0);
char *s = apr_palloc(p, n + 1);
apreq_cookie_serialize(c, s, n + 1);
return s;
}