/* 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 <assert.h>
#include <stdio.h>

#include <apr_lib.h>
#include <apr_file_info.h>
#include <apr_file_io.h>
#include <apr_fnmatch.h>
#include <apr_hash.h>
#include <apr_strings.h>
#include <apr_tables.h>

#include "md.h"
#include "md_crypt.h"
#include "md_json.h"
#include "md_jws.h"
#include "md_log.h"
#include "md_store.h"
#include "md_util.h"
#include "md_version.h"

#include "md_acme.h"
#include "md_acme_acct.h"

static apr_status_t acct_make(md_acme_acct_t **pacct, apr_pool_t *p, 
                              const char *ca_url, apr_array_header_t *contacts)
{
    md_acme_acct_t *acct;
    
    acct = apr_pcalloc(p, sizeof(*acct));

    acct->ca_url = ca_url;
    if (!contacts || apr_is_empty_array(contacts)) {
        acct->contacts = apr_array_make(p, 5, sizeof(const char *));
    }
    else {
        acct->contacts = apr_array_copy(p, contacts);
    }
    
    *pacct = acct;
    return APR_SUCCESS;
}


static const char *mk_acct_id(apr_pool_t *p, md_acme_t *acme, int i)
{
    return apr_psprintf(p, "ACME-%s-%04d", acme->sname, i);
}

static const char *mk_acct_pattern(apr_pool_t *p, md_acme_t *acme)
{
    return apr_psprintf(p, "ACME-%s-*", acme->sname);
}
 
/**************************************************************************************************/
/* json load/save */

static md_acme_acct_st acct_st_from_str(const char *s) 
{
    if (s) {
        if (!strcmp("valid", s)) {
            return MD_ACME_ACCT_ST_VALID;
        }
        else if (!strcmp("deactivated", s)) {
            return MD_ACME_ACCT_ST_DEACTIVATED;
        }
        else if (!strcmp("revoked", s)) {
            return MD_ACME_ACCT_ST_REVOKED;
        }
    }
    return MD_ACME_ACCT_ST_UNKNOWN;
}

md_json_t *md_acme_acct_to_json(md_acme_acct_t *acct, apr_pool_t *p)
{
    md_json_t *jacct;
    const char *s;

    assert(acct);
    jacct = md_json_create(p);
    switch (acct->status) {
        case MD_ACME_ACCT_ST_VALID:
            s = "valid";
            break;
        case MD_ACME_ACCT_ST_DEACTIVATED:
            s = "deactivated";
            break;
        case MD_ACME_ACCT_ST_REVOKED:
            s = "revoked";
            break;
        default:
            s = NULL;
            break;
    }    
    if (s) md_json_sets(s, jacct, MD_KEY_STATUS, NULL);
    if (acct->url) md_json_sets(acct->url, jacct, MD_KEY_URL, NULL);
    if (acct->ca_url) md_json_sets(acct->ca_url, jacct, MD_KEY_CA_URL, NULL);
    if (acct->contacts) md_json_setsa(acct->contacts, jacct, MD_KEY_CONTACT, NULL);
    if (acct->registration) md_json_setj(acct->registration, jacct, MD_KEY_REGISTRATION, NULL);
    if (acct->agreement) md_json_sets(acct->agreement, jacct, MD_KEY_AGREEMENT, NULL);
    if (acct->orders) md_json_sets(acct->orders, jacct, MD_KEY_ORDERS, NULL);
    
    return jacct;
}

apr_status_t md_acme_acct_from_json(md_acme_acct_t **pacct, md_json_t *json, apr_pool_t *p)
{
    apr_status_t rv = APR_EINVAL;
    md_acme_acct_t *acct;
    md_acme_acct_st status = MD_ACME_ACCT_ST_UNKNOWN;
    const char *ca_url, *url;
    apr_array_header_t *contacts;
    
    if (md_json_has_key(json, MD_KEY_STATUS, NULL)) {
        status = acct_st_from_str(md_json_gets(json, MD_KEY_STATUS, NULL));
    }
    else {
        /* old accounts only had disabled boolean field */
        status = md_json_getb(json, MD_KEY_DISABLED, NULL)? 
            MD_ACME_ACCT_ST_DEACTIVATED : MD_ACME_ACCT_ST_VALID;
    }
    
    url = md_json_gets(json, MD_KEY_URL, NULL);
    if (!url) {
        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "account has no url");
        goto out;
    }

    ca_url = md_json_gets(json, MD_KEY_CA_URL, NULL);
    if (!ca_url) {
        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "account has no CA url: %s", url);
        goto out;
    }
    
    contacts = apr_array_make(p, 5, sizeof(const char *));
    if (md_json_has_key(json, MD_KEY_CONTACT, NULL)) {
        md_json_getsa(contacts, json, MD_KEY_CONTACT, NULL);
    }
    else {
        md_json_getsa(contacts, json, MD_KEY_REGISTRATION, MD_KEY_CONTACT, NULL);
    }
    rv = acct_make(&acct, p, ca_url, contacts);
    if (APR_SUCCESS == rv) {
        acct->status = status;
        acct->url = url;
        acct->agreement = md_json_gets(json, "terms-of-service", NULL);
        acct->orders = md_json_gets(json, MD_KEY_ORDERS, NULL);
    }

out:
    *pacct = (APR_SUCCESS == rv)? acct : NULL;
    return rv;
}

apr_status_t md_acme_acct_save(md_store_t *store, apr_pool_t *p, md_acme_t *acme, 
                               const char **pid, md_acme_acct_t *acct, md_pkey_t *acct_key)
{
    md_json_t *jacct;
    apr_status_t rv;
    int i;
    const char *id = pid? *pid : NULL;
    
    jacct = md_acme_acct_to_json(acct, p);
    if (id) {
        rv = md_store_save(store, p, MD_SG_ACCOUNTS, id, MD_FN_ACCOUNT, MD_SV_JSON, jacct, 0);
    }
    else {
        rv = APR_EAGAIN;
        for (i = 0; i < 1000 && APR_SUCCESS != rv; ++i) {
            id = mk_acct_id(p, acme, i);
            rv = md_store_save(store, p, MD_SG_ACCOUNTS, id, MD_FN_ACCOUNT, MD_SV_JSON, jacct, 1);
        }
    }
    if (APR_SUCCESS == rv) {
        if (pid) *pid = id;
        rv = md_store_save(store, p, MD_SG_ACCOUNTS, id, MD_FN_ACCT_KEY, MD_SV_PKEY, acct_key, 0);
    }
    return rv;
}

apr_status_t md_acme_acct_load(md_acme_acct_t **pacct, md_pkey_t **ppkey,
                               md_store_t *store, md_store_group_t group, 
                               const char *name, apr_pool_t *p)
{
    md_json_t *json;
    apr_status_t rv;

    rv = md_store_load_json(store, group, name, MD_FN_ACCOUNT, &json, p);
    if (APR_STATUS_IS_ENOENT(rv)) {
        goto out;
    }
    if (APR_SUCCESS != rv) {
        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "error reading account: %s", name);
        goto out;
    }
    
    rv = md_acme_acct_from_json(pacct, json, p);
    if (APR_SUCCESS == rv) {
        rv = md_store_load(store, group, name, MD_FN_ACCT_KEY, MD_SV_PKEY, (void**)ppkey, p);
        if (APR_SUCCESS != rv) {
            md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "loading key: %s", name);
            goto out;
        }
    }
out:
    if (APR_SUCCESS != rv) {
        *pacct = NULL;
        *ppkey = NULL;
    } 
    return rv;
}

/**************************************************************************************************/
/* Lookup */

typedef struct {
    apr_pool_t *p;
    md_acme_t *acme;
    int url_match;
    const char *id;
} find_ctx;

static int find_acct(void *baton, const char *name, const char *aspect,
                     md_store_vtype_t vtype, void *value, apr_pool_t *ptemp)
{
    find_ctx *ctx = baton;
    int disabled;
    const char *ca_url, *status;
    
    (void)aspect;
    (void)ptemp;
    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ctx->p, "account candidate %s/%s", name, aspect); 
    if (MD_SV_JSON == vtype) {
        md_json_t *json = value;
        
        status = md_json_gets(json, MD_KEY_STATUS, NULL);
        disabled = md_json_getb(json, MD_KEY_DISABLED, NULL);
        ca_url = md_json_gets(json, MD_KEY_CA_URL, NULL);
        
        if ((!status || !strcmp("valid", status)) && !disabled 
            && (!ctx->url_match || (ca_url && !strcmp(ctx->acme->url, ca_url)))) {
            md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ctx->p, 
                          "found account %s for %s: %s, status=%s, disabled=%d, ca-url=%s", 
                          name, ctx->acme->url, aspect, status, disabled, ca_url);
            ctx->id = apr_pstrdup(ctx->p, name);
            return 0;
        }
    }
    return 1;
}

static apr_status_t acct_find(const char **pid, md_acme_acct_t **pacct, md_pkey_t **ppkey, 
                              md_store_t *store, md_store_group_t group,
                              const char *name_pattern, int url_match, 
                              md_acme_t *acme, apr_pool_t *p)
{
    apr_status_t rv;
    find_ctx ctx;
    
    ctx.p = p;
    ctx.acme = acme;
    ctx.id = NULL;
    ctx.url_match = url_match;
    *pid = NULL;
    
    rv = md_store_iter(find_acct, &ctx, store, p, group, name_pattern, MD_FN_ACCOUNT, MD_SV_JSON);
    if (ctx.id) {
        *pid = ctx.id;
        rv = md_acme_acct_load(pacct, ppkey, store, group, ctx.id, p);
        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "loading account %s", ctx.id);
    }
    else {
        *pacct = NULL;
        rv = APR_ENOENT;
        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, p, "acct_find: none found"); 
    }
    return rv;
}

static apr_status_t acct_find_and_verify(md_store_t *store, md_store_group_t group, 
                                         const char *name_pattern, md_acme_t *acme, apr_pool_t *p)
{
    md_acme_acct_t *acct;
    md_pkey_t *pkey;
    const char *id;
    apr_status_t rv;

    if (APR_SUCCESS == (rv = acct_find(&id, &acct, &pkey, store, group, name_pattern, 1, acme, p))) {
        acme->acct_id = (MD_SG_STAGING == group)? NULL : id;
        acme->acct = acct;
        acme->acct_key = pkey;
        rv = md_acme_acct_validate(acme, NULL, p);
    
        if (APR_SUCCESS != rv) {
            acme->acct_id = NULL;
            acme->acct = NULL;
            acme->acct_key = NULL;
            if (APR_STATUS_IS_ENOENT(rv)) {
                /* verification failed and account has been disabled.
                   Indicate to caller that he may try again. */
                rv = APR_EAGAIN;
            }
        }
    }
    return rv;
}

apr_status_t md_acme_find_acct(md_acme_t *acme, md_store_t *store)
{
    apr_status_t rv;
    
    while (APR_EAGAIN == (rv = acct_find_and_verify(store, MD_SG_ACCOUNTS, 
                                                    mk_acct_pattern(acme->p, acme), 
                                                    acme, acme->p))) {
        /* nop */
    }
    
    if (APR_STATUS_IS_ENOENT(rv)) {
        /* No suitable account found in MD_SG_ACCOUNTS. Maybe a new account
         * can already be found in MD_SG_STAGING? */
        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p, 
                      "no account found, looking in STAGING");
        while (APR_EAGAIN == (rv = acct_find_and_verify(store, MD_SG_STAGING, "*", 
                                                        acme, acme->p))) {
            /* nop */
        }
    }
    return rv;
}

typedef struct {
    apr_pool_t *p;
    const char *url;
    const char *id;
} load_ctx;

static int id_by_url(void *baton, const char *name, const char *aspect,
                     md_store_vtype_t vtype, void *value, apr_pool_t *ptemp)
{
    load_ctx *ctx = baton;
    int disabled;
    const char *acct_url, *status;
    
    (void)aspect;
    (void)ptemp;
    if (MD_SV_JSON == vtype) {
        md_json_t *json = value;
        
        status = md_json_gets(json, MD_KEY_STATUS, NULL);
        disabled = md_json_getb(json, MD_KEY_DISABLED, NULL);
        acct_url = md_json_gets(json, MD_KEY_URL, NULL);
        
        if ((!status || !strcmp("valid", status)) && !disabled 
            && acct_url && !strcmp(ctx->url, acct_url)) {
            md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ctx->p, 
                          "found account %s for url %s: %s, status=%s, disabled=%d", 
                          name, ctx->url, aspect, status, disabled);
            ctx->id = apr_pstrdup(ctx->p, name);
            return 0;
        }
    }
    return 1;
}

apr_status_t md_acme_acct_id_for_url(const char **pid, md_store_t *store, 
                                     md_store_group_t group, const char *url, apr_pool_t *p)
{
    apr_status_t rv;
    load_ctx ctx;
    
    ctx.p = p;
    ctx.url = url;
    ctx.id = NULL;
    
    rv = md_store_iter(id_by_url, &ctx, store, p, group, "*", MD_FN_ACCOUNT, MD_SV_JSON);
    *pid = (APR_SUCCESS == rv)? ctx.id : NULL;
    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "acct_id_by_url %s -> %s", url, *pid);
    return rv;
}

/**************************************************************************************************/
/* acct operation context */
typedef struct {
    md_acme_t *acme;
    apr_pool_t *p;
    const char *agreement;
} acct_ctx_t;

/**************************************************************************************************/
/* acct update */

static apr_status_t on_init_acct_upd(md_acme_req_t *req, void *baton)
{
    (void)baton;
    return md_acme_req_body_init(req, NULL);
} 

static apr_status_t acct_upd(md_acme_t *acme, apr_pool_t *p, 
                             const apr_table_t *hdrs, md_json_t *body, void *baton)
{
    acct_ctx_t *ctx = baton;
    apr_status_t rv = APR_SUCCESS;
    md_acme_acct_t *acct = acme->acct;
    
    if (!acct->url) {
        const char *location = apr_table_get(hdrs, "location");
        if (!location) {
            md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, APR_EINVAL, p, "new acct without location");
            return APR_EINVAL;
        }
        acct->url = apr_pstrdup(ctx->p, location);
    }
    
    apr_array_clear(acct->contacts);
    md_json_dupsa(acct->contacts, acme->p, body, MD_KEY_CONTACT, NULL);
    if (md_json_has_key(body, MD_KEY_STATUS, NULL)) {
        acct->status = acct_st_from_str(md_json_gets(body, MD_KEY_STATUS, NULL));
    }
    if (md_json_has_key(body, MD_KEY_AGREEMENT, NULL)) {
        acct->agreement = md_json_dups(acme->p, body, MD_KEY_AGREEMENT, NULL);
    }
    if (md_json_has_key(body, MD_KEY_ORDERS, NULL)) {
        acct->orders = md_json_dups(acme->p, body, MD_KEY_ORDERS, NULL);
    }
    acct->registration = md_json_clone(ctx->p, body);
    
    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "updated acct %s", acct->url);
    return rv;
}

apr_status_t md_acme_acct_update(md_acme_t *acme)
{
    acct_ctx_t ctx;
    
    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p, "acct update");
    if (!acme->acct) {
        return APR_EINVAL;
    }
    ctx.acme = acme;
    ctx.p = acme->p;
    return md_acme_POST(acme, acme->acct->url, on_init_acct_upd, acct_upd, NULL, NULL, &ctx);
}

apr_status_t md_acme_acct_validate(md_acme_t *acme, md_store_t *store, apr_pool_t *p)
{
    apr_status_t rv;
    
    if (APR_SUCCESS != (rv = md_acme_acct_update(acme))) {
        if (acme->acct && (APR_ENOENT == rv || APR_EACCES == rv)) {
            if (MD_ACME_ACCT_ST_VALID == acme->acct->status) {
                acme->acct->status = MD_ACME_ACCT_ST_UNKNOWN;
                if (store) {
                    md_acme_acct_save(store, p, acme, &acme->acct_id, acme->acct, acme->acct_key); 
                }
            }
            acme->acct = NULL;
            acme->acct_key = NULL;
            rv = APR_ENOENT;
        }
    }
    return rv;
}

/**************************************************************************************************/
/* Register a new account */

static apr_status_t on_init_acct_new(md_acme_req_t *req, void *baton)
{
    acct_ctx_t *ctx = baton;
    md_json_t *jpayload;

    jpayload = md_json_create(req->p);
    md_json_setsa(ctx->acme->acct->contacts, jpayload, MD_KEY_CONTACT, NULL);
    if (ctx->agreement) {
        md_json_setb(1, jpayload, "termsOfServiceAgreed", NULL);
    }
    return md_acme_req_body_init(req, jpayload);
} 

apr_status_t md_acme_acct_register(md_acme_t *acme, md_store_t *store, apr_pool_t *p, 
                                   apr_array_header_t *contacts, const char *agreement)
{
    apr_status_t rv;
    md_pkey_t *pkey;
    const char *err = NULL, *uri;
    md_pkey_spec_t spec;
    int i;
    acct_ctx_t ctx;
    
    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "create new account");
    
    ctx.acme = acme;
    ctx.p = p;
    /* The agreement URL is submitted when the ACME server announces Terms-of-Service
     * in its directory meta data. The magic value "accepted" will always use the
     * advertised URL. */
    ctx.agreement = NULL;
    if (acme->ca_agreement && agreement) {
        ctx.agreement = !strcmp("accepted", agreement)? acme->ca_agreement : agreement;
    }
    
    if (ctx.agreement) {
        if (APR_SUCCESS != (rv = md_util_abs_uri_check(acme->p, ctx.agreement, &err))) {
            md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, 
                          "invalid agreement uri (%s): %s", err, ctx.agreement);
            goto out;
        }
    }
    
    for (i = 0; i < contacts->nelts; ++i) {
        uri = APR_ARRAY_IDX(contacts, i, const char *);
        if (APR_SUCCESS != (rv = md_util_abs_uri_check(acme->p, uri, &err))) {
            md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, 
                          "invalid contact uri (%s): %s", err, uri);
            goto out;
        }
    }
    
    /* If there is no key selected yet, try to find an existing one for the same host. 
     * Let's Encrypt identifies accounts by their key for their ACMEv1 and v2 services.
     * Although the account appears on both services with different urls, it is 
     * internally the same one.
     * I think this is beneficial if someone migrates from ACMEv1 to v2 and not a leak
     * of identifying information.
     */
    if (!acme->acct_key) {
        find_ctx fctx;
    
        fctx.p = p;
        fctx.acme = acme;
        fctx.id = NULL;
        fctx.url_match = 0;
        
        md_store_iter(find_acct, &fctx, store, p, MD_SG_ACCOUNTS, 
                      mk_acct_pattern(p, acme), MD_FN_ACCOUNT, MD_SV_JSON);
        if (fctx.id) {
            rv = md_store_load(store, MD_SG_ACCOUNTS, fctx.id, MD_FN_ACCT_KEY, MD_SV_PKEY, 
                               (void**)&acme->acct_key, p);
            if (APR_SUCCESS == rv) {
                md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "reusing key from account %s", fctx.id);
            }
            else {
                acme->acct_key = NULL;
            }
        }
    }
    
    /* If we still have no key, generate a new one */
    if (!acme->acct_key) {
        spec.type = MD_PKEY_TYPE_RSA;
        spec.params.rsa.bits = MD_ACME_ACCT_PKEY_BITS;
        
        if (APR_SUCCESS != (rv = md_pkey_gen(&pkey, acme->p, &spec))) goto out;
        acme->acct_key = pkey;
        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "created new account key");
    }
    
    if (APR_SUCCESS != (rv = acct_make(&acme->acct,  p, acme->url, contacts))) goto out;
    rv = md_acme_POST_new_account(acme,  on_init_acct_new, acct_upd, NULL, NULL, &ctx);
    if (APR_SUCCESS == rv) {
        md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, p, 
                      "registered new account %s", acme->acct->url);
    }

out:    
    if (APR_SUCCESS != rv && acme->acct) {
        acme->acct = NULL;
    }
    return rv;
}

/**************************************************************************************************/
/* Deactivate the account */

static apr_status_t on_init_acct_del(md_acme_req_t *req, void *baton)
{
    md_json_t *jpayload;

    (void)baton;
    jpayload = md_json_create(req->p);
    md_json_sets("deactivated", jpayload, MD_KEY_STATUS, NULL);
    return md_acme_req_body_init(req, jpayload);
} 

apr_status_t md_acme_acct_deactivate(md_acme_t *acme, apr_pool_t *p)
{
    md_acme_acct_t *acct = acme->acct;
    acct_ctx_t ctx;
    
    (void)p;
    if (!acct) {
        return APR_EINVAL;
    }
    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p, "delete account %s from %s", 
                  acct->url, acct->ca_url);
    ctx.acme = acme;
    ctx.p = p;
    return md_acme_POST(acme, acct->url, on_init_acct_del, acct_upd, NULL, NULL, &ctx);
}

/**************************************************************************************************/
/* terms-of-service */

static apr_status_t on_init_agree_tos(md_acme_req_t *req, void *baton)
{
    acct_ctx_t *ctx = baton;
    md_json_t *jpayload;

    jpayload = md_json_create(req->p);
    if (ctx->acme->acct->agreement) {
        md_json_setb(1, jpayload, "termsOfServiceAgreed", NULL);
    }
    return md_acme_req_body_init(req, jpayload);
} 

apr_status_t md_acme_agree(md_acme_t *acme, apr_pool_t *p, const char *agreement)
{
    acct_ctx_t ctx;
    
    acme->acct->agreement = agreement;
    if (!strcmp("accepted", agreement) && acme->ca_agreement) {
        acme->acct->agreement = acme->ca_agreement;
    }
    
    ctx.acme = acme;
    ctx.p = p;
    return md_acme_POST(acme, acme->acct->url, on_init_agree_tos, acct_upd, NULL, NULL, &ctx);
}

apr_status_t md_acme_check_agreement(md_acme_t *acme, apr_pool_t *p, 
                                     const char *agreement, const char **prequired)
{
    apr_status_t rv = APR_SUCCESS;
    
    /* We used to really check if the account agreement and the one indicated in meta
     * are the very same. However, LE is happy if the account has agreed to a ToS in 
     * the past and does not require a renewed acceptance.
     */
    *prequired = NULL;
    if (!acme->acct->agreement && acme->ca_agreement) {
        if (agreement) {
            rv = md_acme_agree(acme, p, acme->ca_agreement);
        }
        else {
            *prequired = acme->ca_agreement;
            rv = APR_INCOMPLETE;
        }
    }
    return rv;
}        
