/*
 *
 * 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 "proton/ssl.hpp"
#include "proton/error.hpp"
#include "msg.hpp"

#include <proton/ssl.h>

namespace proton {

/// TODO: This whole class isn't really needed as pn_ssl_domain_t is already refcounted, with shared ownership for all pn_ssl_t objects
/// that hold one
/// cjansen: but note it is not a PN_CLASS object and two pn_ssl_domain_free() from the app will cause a free while still in use.
class ssl_domain_impl {
  public:
    ssl_domain_impl(bool is_server) : refcount_(1), pn_domain_(pn_ssl_domain(is_server ? PN_SSL_MODE_SERVER : PN_SSL_MODE_CLIENT)) {
            if (!pn_domain_) throw error(MSG("SSL/TLS unavailable"));
    }
    ~ssl_domain_impl() { pn_ssl_domain_free(pn_domain_); }
    void incref() { refcount_++; }
    void decref() {
        if (--refcount_ == 0) {
            delete this;
        }
    }
    pn_ssl_domain_t *pn_domain() { return pn_domain_; }
  private:
    int refcount_;
    pn_ssl_domain_t *pn_domain_;
    ssl_domain_impl(const ssl_domain_impl&);
    ssl_domain_impl& operator=(const ssl_domain_impl&);
};

namespace internal {
ssl_domain::ssl_domain(bool is_server) : impl_(0), server_type_(is_server) {}

ssl_domain::ssl_domain(const ssl_domain &x) : impl_(x.impl_), server_type_(x.server_type_) {
    if (impl_) impl_->incref();
}

ssl_domain& internal::ssl_domain::operator=(const ssl_domain&x) {
    if (&x == this) return *this;
    if (impl_) impl_->decref();
    impl_ = x.impl_;
    server_type_ = x.server_type_;
    if (impl_) impl_->incref();
    return *this;
}

ssl_domain::~ssl_domain() { if (impl_) impl_->decref(); }

pn_ssl_domain_t *ssl_domain::pn_domain() {
    if (!impl_)
        // Lazily create in case never actually used or configured.
        // The pn_ssl_domain_t is a heavy object.
        impl_ = new ssl_domain_impl(server_type_);
    return impl_->pn_domain();
}

} // namespace internal

namespace {

void set_cred(pn_ssl_domain_t *dom, const std::string &main, const std::string &extra, const std::string &pass, bool pwset) {
    const char *cred2 = extra.empty() ? NULL : extra.c_str();
    const char *pw = pwset ? pass.c_str() : NULL;
    if (pn_ssl_domain_set_credentials(dom, main.c_str(), cred2, pw))
        throw error(MSG("SSL certificate initialization failure for " << main << ":" <<
                        (cred2 ? cred2 : "NULL") << ":" << (pw ? pw : "NULL")));
}
}

ssl_server_options::ssl_server_options(ssl_certificate &cert) : internal::ssl_domain(true) {
    set_cred(pn_domain(), cert.certdb_main_, cert.certdb_extra_, cert.passwd_, cert.pw_set_);
}

ssl_server_options::ssl_server_options(
    ssl_certificate &cert,
    const std::string &trust_db,
    const std::string &advertise_db,
    enum ssl::verify_mode mode) : internal::ssl_domain(true)
{
    pn_ssl_domain_t* dom = pn_domain();
    set_cred(dom, cert.certdb_main_, cert.certdb_extra_, cert.passwd_, cert.pw_set_);
    if (pn_ssl_domain_set_trusted_ca_db(dom, trust_db.c_str()))
        throw error(MSG("SSL trust store initialization failure for " << trust_db));
    const std::string &db = advertise_db.empty() ? trust_db : advertise_db;
    if (pn_ssl_domain_set_peer_authentication(dom, pn_ssl_verify_mode_t(mode), db.c_str()))
        throw error(MSG("SSL server configuration failure requiring client certificates using " << db));
}

ssl_server_options::ssl_server_options() : ssl_domain(true) {}

namespace {
void client_setup(pn_ssl_domain_t *dom, const std::string &trust_db, enum ssl::verify_mode mode) {
    if (pn_ssl_domain_set_trusted_ca_db(dom, trust_db.c_str()))
        throw error(MSG("SSL trust store initialization failure for " << trust_db));
    if (pn_ssl_domain_set_peer_authentication(dom, pn_ssl_verify_mode_t(mode), NULL))
        throw error(MSG("SSL client verify mode failure"));
}
}

ssl_client_options::ssl_client_options(const std::string &trust_db, enum ssl::verify_mode mode) : ssl_domain(false) {
    client_setup(pn_domain(), trust_db, mode);
}

ssl_client_options::ssl_client_options(ssl_certificate &cert, const std::string &trust_db, enum ssl::verify_mode mode) : ssl_domain(false) {
    pn_ssl_domain_t *dom = pn_domain();
    set_cred(dom, cert.certdb_main_, cert.certdb_extra_, cert.passwd_, cert.pw_set_);
    client_setup(dom, trust_db, mode);
}

ssl_client_options::ssl_client_options() : ssl_domain(false) {}

ssl_certificate::ssl_certificate(const std::string &main)
    : certdb_main_(main), pw_set_(false) {}

ssl_certificate::ssl_certificate(const std::string &main, const std::string &extra)
    : certdb_main_(main), certdb_extra_(extra), pw_set_(false) {}

ssl_certificate::ssl_certificate(const std::string &main, const std::string &extra, const std::string &pw)
    : certdb_main_(main), certdb_extra_(extra), passwd_(pw), pw_set_(true) {}

} // namespace
