/*
 * Copyright (c) 2015 DeNA Co., Ltd., Kazuho Oku
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 */
#include "../test.h"
#include "../../../src/ssl.c"

static int encrypt_and_decrypt_ticket_once(ptls_encrypt_ticket_t *encryptor, ptls_encrypt_ticket_t *decryptor)
{
    static const char plaintext[] = "hello world";
    ptls_buffer_t encrypted, decrypted;
    int ret;

    /* encrypt */
    ptls_buffer_init(&encrypted, "", 0);
    ret = encryptor->cb(encryptor, NULL, 1, &encrypted, ptls_iovec_init(plaintext, strlen(plaintext)));
    ok(ret == 0);

    /* decrypt: If result is failure, return the error code to let the caller test. Otherwise, validate decrypted text. */
    ptls_buffer_init(&decrypted, "", 0);
    if ((ret = decryptor->cb(decryptor, NULL, 0, &decrypted, ptls_iovec_init(encrypted.base, encrypted.off))) != 0)
        return ret;
    if (decrypted.off != strlen(plaintext))
        return PTLS_ALERT_DECODE_ERROR;
    ok(memcmp(decrypted.base, plaintext, strlen(plaintext)) == 0);
    ptls_buffer_dispose(&encrypted);
    ptls_buffer_dispose(&decrypted);

    return 0;
}

static void test_encrypt_ticket_ptls(void)
{
    /* create three encryptors, TLS, QUIC(spec_context), QUIC(another_context) */
    quicly_context_t alt_quic = quicly_spec_context;
    ++alt_quic.transport_params.max_streams_uni;
    ptls_encrypt_ticket_t *tls_enc = create_encrypt_ticket_ptls(NULL),
                          *quic_enc1 = create_encrypt_ticket_ptls(&quicly_spec_context),
                          *quic_enc2 = create_encrypt_ticket_ptls(&alt_quic);

    /* check that ticket decryption succeeds when the conditions match */
    ok(encrypt_and_decrypt_ticket_once(tls_enc, tls_enc) == 0);
    ok(encrypt_and_decrypt_ticket_once(quic_enc1, quic_enc1) == 0);
    ok(encrypt_and_decrypt_ticket_once(quic_enc2, quic_enc2) == 0);

    /* but that 0-RTT is rejected when the conditions do not match */
    ok(encrypt_and_decrypt_ticket_once(tls_enc, quic_enc1) ==
       PTLS_ERROR_REJECT_EARLY_DATA); /* TP mismatch detected before content mismatch */
    ok(encrypt_and_decrypt_ticket_once(quic_enc1, tls_enc) == PTLS_ALERT_DECODE_ERROR);
    ok(encrypt_and_decrypt_ticket_once(quic_enc1, quic_enc2) == PTLS_ERROR_REJECT_EARLY_DATA);
}

const uint64_t UTC2000 = (365 * 30 + 7) * 86400;

static void test_load_tickets_file(void)
{
    int ret = load_tickets_file("t/assets/session_tickets.yaml");
    ok(ret == 0);
    if (ret != 0)
        return;

    pthread_rwlock_rdlock(&session_tickets.rwlock);
    ok(session_tickets.tickets.size == 2);
    if (session_tickets.tickets.size != 2)
        goto Exit;

    /* first entry should be the newer one */
    struct st_session_ticket_t *ticket = session_tickets.tickets.entries[0];
    ok(memcmp(ticket->name, H2O_STRLIT("\xe7\xe3\xc6\x98\x0b\x18\x32\xbd\x5d\x23\x91\x75\x72\xe8\x44\x8f")) == 0);
    ok(ticket->cipher == EVP_aes_256_cbc());
    ok(memcmp(session_ticket_get_cipher_key(ticket),
              H2O_STRLIT("\xf6\xe0\x71\xd9\x93\xb0\x5f\x77\xce\x51\xcb\x0f\xe2\xe0\xe1\x8c\x72\x00\xc2\xa7"
                         "\x87\x3a\x66\x00\x8c\x8e\x1d\x75\xae\x7b\x8e\x2a")) == 0);
    ok(ticket->hmac == EVP_sha256());
    ok(memcmp(session_ticket_get_hmac_key(ticket),
              H2O_STRLIT("\xf4\xfc\xb8\x6f\xdf\x03\xa7\xf3\x35\x63\x2e\x66\x8a\x8f\xe9\x56\xc5\xbf\xe7\x7a\x41\x41\x2d\x26\x99"
                         "\x79\x63\x47\x68\x99\x9a\xdd\x6a\x84\xca\xfe\xa4\x1b\x6b\x2c\x47\xaa\xf1\xa5\x14\xca\x9d\x2a\x84\xf4"
                         "\x8d\x1f\x5f\x70\x18\xff\x17\x40\xcf\x9b\x94\x4b\x8f\xcf")) == 0);
    ok(ticket->not_before == 1437093330);
    ok(ticket->not_after == 1437096929);

    /* second is the older one */
    ticket = session_tickets.tickets.entries[1];
    ok(memcmp(ticket->name, H2O_STRLIT("\xa3\x97\xb6\xb7\xfa\xb9\x29\x36\x62\x03\xf1\x6f\xc8\x1f\xfb\xed")) == 0);
    ok(ticket->cipher == EVP_aes_128_cbc());
    ok(memcmp(session_ticket_get_cipher_key(ticket),
              H2O_STRLIT("\xf1\xed\x89\xcd\xe6\x87\x63\x63\x0e\x80\xd2\xbe\x82\x7c\xfb\x98")) == 0);
    ok(ticket->hmac == EVP_sha1());
    ok(memcmp(session_ticket_get_hmac_key(ticket),
              H2O_STRLIT("\xe3\xfe\x72\x64\x4f\x64\x31\x5a\x4a\x8a\xd6\x37\x69\xa3\x57\x7c\xce\xc4\xdd\x13\xb2\x0e\xaf\x8c\x00\x88"
                         "\x86\xe5\x45\x8d\xb1\x0e\x65\x8c\xf2\xa8\x3f\x04\x40\x3a\xc4\xe9\x80\x35\xd2\x42\x2a\x75\x80\x67\x30\xeb"
                         "\x4f\x2f\xee\x12\xfa\xff\x95\x48\x95\xbc\x65\xd1")) == 0);
    ok(ticket->not_before == 1437092430);
    ok(ticket->not_after == 1437096029);

    ticket = find_ticket_for_encryption(&session_tickets.tickets, 1437092429);
    ok(ticket == NULL);
    ticket = find_ticket_for_encryption(&session_tickets.tickets, 1437092430);
    ok(ticket == session_tickets.tickets.entries[1]);
    ticket = find_ticket_for_encryption(&session_tickets.tickets, 1437093329);
    ok(ticket == session_tickets.tickets.entries[1]);
    ticket = find_ticket_for_encryption(&session_tickets.tickets, 1437093330);
    ok(ticket == session_tickets.tickets.entries[0]);
    ticket = find_ticket_for_encryption(&session_tickets.tickets, 1437096029);
    ok(ticket == session_tickets.tickets.entries[0]);
    ticket = find_ticket_for_encryption(&session_tickets.tickets, 1437096030);
    ok(ticket == session_tickets.tickets.entries[0]);
    ticket = find_ticket_for_encryption(&session_tickets.tickets, 1437096929);
    ok(ticket == session_tickets.tickets.entries[0]);
    ticket = find_ticket_for_encryption(&session_tickets.tickets, 1437096930);
    ok(ticket == NULL);

Exit:
    pthread_rwlock_unlock(&session_tickets.rwlock);
    ;
}

static void test_serialize_tickets(void)
{
    session_ticket_vector_t orig = {NULL}, parsed = {NULL};
    h2o_iovec_t serialized;
    char errstr[256];
    int ret;
    size_t i;

    h2o_vector_reserve(NULL, &orig, orig.size + 2);
    orig.entries[orig.size++] = new_ticket(EVP_aes_256_cbc(), EVP_sha256(), UTC2000, UTC2000 + 3600, 1);
    orig.entries[orig.size++] = new_ticket(EVP_aes_256_cbc(), EVP_sha256(), UTC2000 + 600, UTC2000 + 4200, 1);

    serialized = serialize_tickets(&orig);
    ok(serialized.base != NULL);

    ret = parse_tickets(&parsed, serialized.base, serialized.len, errstr);
    ok(ret == 0);

    ok(parsed.size == orig.size);
    for (i = 0; i != parsed.size; ++i) {
#define OK_VALUE(n) ok(parsed.entries[i]->n == orig.entries[i]->n)
#define OK_MEMCMP(n, s) ok(memcmp(parsed.entries[i]->n, orig.entries[i]->n, (s)) == 0)
#define OK_MEMCMPF(f, s) ok(memcmp(f(parsed.entries[i]), f(orig.entries[i]), (s)) == 0)
        OK_MEMCMP(name, sizeof(parsed.entries[i]->name));
        OK_VALUE(cipher);
        OK_MEMCMPF(session_ticket_get_cipher_key, EVP_CIPHER_key_length(parsed.entries[i]->cipher));
        OK_VALUE(hmac);
        OK_MEMCMPF(session_ticket_get_hmac_key, EVP_MD_block_size(parsed.entries[i]->hmac));
        OK_VALUE(not_before);
        OK_VALUE(not_after);
#undef OK_VALUE
#undef OK_MEMCMP
#undef OK_MEMCMPF
    }

    free_tickets(&orig);
    free_tickets(&parsed);
    free(serialized.base);
}

static void test_memcached_ticket_update(void)
{
#define TEST_KEY "h2o:session-ticket-test"

    const char *memc_port_str;
    uint16_t memc_port;
    yrmcds conn;
    yrmcds_response resp;
    yrmcds_error err;

    /* obtain port number (or skip) */
    if ((memc_port_str = getenv("MEMCACHED_PORT")) == NULL) {
        printf("MEMCACHED_PORT is not defined; skipping tests\n");
        return;
    }
    if (sscanf(memc_port_str, "%" SCNu16, &memc_port) != 1) {
        fprintf(stderr, "failed to parse the value of MEMCACHED_PORT\n");
        ok(0);
        return;
    }
    /* connect */
    err = yrmcds_connect(&conn, "127.0.0.1", memc_port);
    ok(err == YRMCDS_OK);
    if (err != YRMCDS_OK)
        return;
    /* delete test key */
    err = yrmcds_remove(&conn, H2O_STRLIT(TEST_KEY), 0, NULL);
    ok(err == YRMCDS_OK);
    if (err != YRMCDS_OK)
        return;
    err = yrmcds_recv(&conn, &resp);
    ok(err == YRMCDS_OK);
    if (err != YRMCDS_OK)
        return;

    /* set a new entry that immediately becomes active */
    int retry = ticket_memcached_update_tickets(&conn, h2o_iovec_init(H2O_STRLIT(TEST_KEY)), UTC2000);
    ok(retry == 1); /* first attempt should return a retry, since valid ticket does not exist */
    retry = ticket_memcached_update_tickets(&conn, h2o_iovec_init(H2O_STRLIT(TEST_KEY)), UTC2000 + 1);
    ok(retry == 0);
    ok(session_tickets.tickets.size == 1);
    ok(session_tickets.tickets.entries[0]->not_before == UTC2000);

    /* continue using existing one */
    retry = ticket_memcached_update_tickets(&conn, h2o_iovec_init(H2O_STRLIT(TEST_KEY)), UTC2000 + conf.lifetime / 8);
    ok(retry == 0);
    ok(session_tickets.tickets.size == 1);
    ok(session_tickets.tickets.entries[0]->not_before == UTC2000);

    /* schedule a new entry */
    retry = ticket_memcached_update_tickets(&conn, h2o_iovec_init(H2O_STRLIT(TEST_KEY)), UTC2000 + conf.lifetime / 2);
    ok(retry == 1);
    retry = ticket_memcached_update_tickets(&conn, h2o_iovec_init(H2O_STRLIT(TEST_KEY)), UTC2000 + conf.lifetime / 2);
    ok(retry == 0);
    ok(session_tickets.tickets.size == 2);
    ok(session_tickets.tickets.entries[0]->not_before > UTC2000 + conf.lifetime / 2);
    ok(session_tickets.tickets.entries[1]->not_before == UTC2000);

    /* old entry gets removed when expired, and new entry is scheduled */
    retry = ticket_memcached_update_tickets(&conn, h2o_iovec_init(H2O_STRLIT(TEST_KEY)), UTC2000 + conf.lifetime);
    ok(retry == 1);
    retry = ticket_memcached_update_tickets(&conn, h2o_iovec_init(H2O_STRLIT(TEST_KEY)), UTC2000 + conf.lifetime);
    ok(retry == 0);
    ok(session_tickets.tickets.size == 2);
    ok(session_tickets.tickets.entries[0]->not_before > UTC2000 + conf.lifetime);
    ok(session_tickets.tickets.entries[1]->not_before > UTC2000 + conf.lifetime / 2);

    /* disconnect */
    yrmcds_close(&conn);
}

void test_src__ssl_c(void)
{
    assert(conf.ticket.update_thread != NULL);
    do_internal_update(NULL);
    subtest("encrypt-ticket-ptls", test_encrypt_ticket_ptls);
    session_tickets.tickets.size = 0;

    subtest("load-tickets-file", test_load_tickets_file);
    subtest("serialize-tickets", test_serialize_tickets);
    subtest("memcached-ticket-update", test_memcached_ticket_update);
}
