The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
/*
**  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;
}