/*
 * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2 of the License, or (at your
 * option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program. If not, see .
 */
#include "SslContext.h"
#include "Config.h"
#include "Log.h"
#include "Memory.h"
#include 
#include 
#include 
#include 
bool Battlenet::SslContext::_usesDevWildcardCertificate = false;
namespace
{
auto CreatePasswordUiMethodFromPemCallback(::pem_password_cb* callback)
{
    return Trinity::make_unique_ptr_with_deleter(UI_UTIL_wrap_read_pem_callback(callback, 0), &::UI_destroy_method);
}
auto OpenOpenSSLStore(boost::filesystem::path const& storePath, UI_METHOD const* passwordCallback, void* passwordCallbackData)
{
    std::string uri;
    uri.reserve(6 + storePath.size());
    uri += "file:";
    std::string genericPath = storePath.generic_string();
    if (!genericPath.empty() && !genericPath.starts_with('/'))
        uri += '/'; // ensure the path starts with / (windows special case, unix absolute paths already do)
    uri += genericPath;
    return Trinity::make_unique_ptr_with_deleter(OSSL_STORE_open(uri.c_str(), passwordCallback, passwordCallbackData, nullptr, nullptr), &::OSSL_STORE_close);
}
boost::system::error_code GetLastOpenSSLError()
{
    auto ossl_error = ::ERR_get_error();
    if (ERR_SYSTEM_ERROR(ossl_error))
        return boost::system::error_code(static_cast(::ERR_GET_REASON(ossl_error)), boost::asio::error::get_system_category());
    return boost::system::error_code(static_cast(ossl_error), boost::asio::error::get_ssl_category());
}
}
bool Battlenet::SslContext::Initialize()
{
    boost::system::error_code err;
#define LOAD_CHECK(fn) do { fn; \
    if (err) \
    { \
        TC_LOG_ERROR("server.ssl", #fn " failed: {}", err.message()); \
        return false; \
    } } while (0)
    std::string certificateChainFile = sConfigMgr->GetStringDefault("CertificatesFile", "./bnetserver.cert.pem");
    auto passwordCallback = [](std::size_t /*max_length*/, boost::asio::ssl::context::password_purpose /*purpose*/) -> std::string
    {
        return sConfigMgr->GetStringDefault("PrivateKeyPassword", "");
    };
    LOAD_CHECK(instance().set_password_callback(passwordCallback, err));
    SSL_CTX* nativeContext = instance().native_handle();
    auto password_ui_method = CreatePasswordUiMethodFromPemCallback(SSL_CTX_get_default_passwd_cb(nativeContext));
    auto store = OpenOpenSSLStore(boost::filesystem::absolute(certificateChainFile),
        password_ui_method.get(), SSL_CTX_get_default_passwd_cb_userdata(nativeContext));
    if (!store)
    {
        err = GetLastOpenSSLError();
        TC_LOG_ERROR("server.ssl", "OSSL_STORE_open failed: {}", err.message());
        return false;
    }
    EVP_PKEY* key = nullptr;
    STACK_OF(X509)* certs = sk_X509_new_null();
    while (!OSSL_STORE_eof(store.get()))
    {
        OSSL_STORE_INFO* info = OSSL_STORE_load(store.get());
        if (!info)
            continue;
        switch (OSSL_STORE_INFO_get_type(info))
        {
            case OSSL_STORE_INFO_PKEY:
                key = OSSL_STORE_INFO_get1_PKEY(info);
                break;
            case OSSL_STORE_INFO_CERT:
                sk_X509_push(certs, OSSL_STORE_INFO_get1_CERT(info));
                break;
            default:
                break;
        }
    }
    if (sk_X509_num(certs) > 0)
    {
        X509* cert = sk_X509_shift(certs);
        _usesDevWildcardCertificate = [&]
        {
            X509_NAME* nm = X509_get_subject_name(cert);
            int32 lastpos = -1;
            while (true)
            {
                lastpos = X509_NAME_get_index_by_NID(nm, NID_commonName, lastpos);
                if (lastpos == -1)
                    break;
                X509_NAME_ENTRY* e = X509_NAME_get_entry(nm, lastpos);
                if (!e)
                    continue;
                ASN1_STRING* text = X509_NAME_ENTRY_get_data(e);
                if (!text)
                    continue;
                unsigned char* utf8TextRaw = nullptr;
                if (int utf8Length = ASN1_STRING_to_UTF8(&utf8TextRaw, text); utf8Length >= 0)
                {
                    auto utf8Text = Trinity::make_unique_ptr_with_deleter(utf8TextRaw, [](unsigned char* ptr) { ::OPENSSL_free(ptr); });
                    if (std::string_view(reinterpret_cast(utf8Text.get()), utf8Length) == "*.*")
                        return true;
                }
            }
            return false;
        }();
        SSL_CTX_use_cert_and_key(nativeContext, cert, key, certs, 1);
    }
    sk_X509_free(certs);
    if (!key)
    {
        std::string privateKeyFile = sConfigMgr->GetStringDefault("PrivateKeyFile", "./bnetserver.key.pem");
        LOAD_CHECK(instance().use_private_key_file(privateKeyFile, boost::asio::ssl::context::pem, err));
    }
#undef LOAD_CHECK
    return true;
}
boost::asio::ssl::context& Battlenet::SslContext::instance()
{
    static boost::asio::ssl::context context(boost::asio::ssl::context::tls);
    return context;
}