/*
 * The OpenSAML License, Version 1.
 * Copyright (c) 2002
 * University Corporation for Advanced Internet Development, Inc.
 * All rights reserved
 *
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution, if any, must include
 * the following acknowledgment: "This product includes software developed by
 * the University Corporation for Advanced Internet Development
 * <http://www.ucaid.edu>Internet2 Project. Alternately, this acknowledegement
 * may appear in the software itself, if and wherever such third-party
 * acknowledgments normally appear.
 *
 * Neither the name of OpenSAML nor the names of its contributors, nor
 * Internet2, nor the University Corporation for Advanced Internet Development,
 * Inc., nor UCAID may be used to endorse or promote products derived from this
 * software without specific prior written permission. For written permission,
 * please contact opensaml@opensaml.org
 *
 * Products derived from this software may not be called OpenSAML, Internet2,
 * UCAID, or the University Corporation for Advanced Internet Development, nor
 * may OpenSAML appear in their name, without prior written permission of the
 * University Corporation for Advanced Internet Development.
 *
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
 * PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE DISCLAIMED AND THE ENTIRE RISK
 * OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS WITH LICENSEE.
 * IN NO EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY
 * CORPORATION FOR ADVANCED INTERNET DEVELOPMENT, INC. BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


/* SAMLSOAPBinding.cpp - basic SAML SOAP binding implementation

   Scott Cantor
   6/3/02

   $History:$
*/

#include "internal.h"
using namespace saml;

#include <ctime>
#include <sstream>
using namespace std;

#include <curl/curl.h>
#include <log4cpp/Category.hh>
#include <xercesc/framework/MemBufInputSource.hpp>
#include <xercesc/util/Base64.hpp>

SAMLSOAPBinding::SAMLSOAPBinding() {}

SAMLSOAPBinding::~SAMLSOAPBinding() {}

size_t curl_write_hook(void *ptr, size_t size, size_t nmemb, void *stream)
{
    // *stream is actually an ostream object
    ostream& buf=*(reinterpret_cast<ostream*>(stream));
    buf.write(reinterpret_cast<const char*>(ptr),size*nmemb);
    return size*nmemb;
}

#define CHECK_CURL_REQ(c) \
    if (c!=CURLE_OK) \
        throw BindingException(SAMLException::REQUESTER, \
            string("SAMLSOAPBinding::send() failed while contacting AA: ") + \
            (curl_errorbuf[0] ? curl_errorbuf : "no further information available"))

#define CHECK_CURL_RESP(c) \
    if (c!=CURLE_OK) \
        throw BindingException(SAMLException::RESPONDER, \
            string("SAMLSOAPBinding::send() failed while contacting AA: ") + \
            (curl_errorbuf[0] ? curl_errorbuf : "no further information available"))

SAMLResponse* SAMLSOAPBinding::send(const SAMLAuthorityBinding& bindingInfo, SAMLRequest& req, const SAMLConfig::SAMLBindingConfig& conf)
{
    NDC ndc("send");
    Category& log=Category::getInstance(SAML_LOGCAT".SAMLSOAPBinding");

    // Turn the request into a DOM, and use its document for the SOAP nodes.
    DOMDocument* doc=req.toDOM()->getOwnerDocument();

    // Build a SOAP envelope and body.
    DOMElement* e=doc->createElementNS(XML::SOAP11ENV_NS,L(Envelope));
    e->setAttribute(L(xmlns),XML::SOAP11ENV_NS);
    doc->appendChild(e);
    DOMElement* body=doc->createElementNS(XML::SOAP11ENV_NS,L(Body));
    e->appendChild(body);

    // Attach SAML request and give memory deallocation control to the request.
    body->appendChild(req.toDOM());

    // Serialize the DOM.
    stringstream inbuf;
    ostringstream outbuf;
    inbuf << *(doc->getDocumentElement());

    char curl_errorbuf[CURL_ERROR_SIZE];
    curl_errorbuf[0]=0;
    
    CURL* m_curl=curl_easy_init();
    if (!m_curl)
        throw SAMLException(SAMLException::REQUESTER,"SAMLSOAPBinding::send() unable to obtain a curl handle");

    // Setup the curl properties.
    string inbufstring=inbuf.str();
    log.debug("input to AA: %s",inbufstring.c_str());
    CURLcode cc=curl_easy_setopt(m_curl,CURLOPT_POSTFIELDS,inbufstring.c_str());
    CHECK_CURL_REQ(cc);
    cc=curl_easy_setopt(m_curl,CURLOPT_POSTFIELDSIZE,inbufstring.length());
    CHECK_CURL_REQ(cc);
    cc=curl_easy_setopt(m_curl,CURLOPT_WRITEFUNCTION,&curl_write_hook);
    CHECK_CURL_REQ(cc);
    cc=curl_easy_setopt(m_curl,CURLOPT_FILE,&outbuf);
    CHECK_CURL_REQ(cc);

    if (log.isDebugEnabled())
    {
        cc=curl_easy_setopt(m_curl,CURLOPT_VERBOSE,1);
        CHECK_CURL_REQ(cc);
    }

    cc=curl_easy_setopt(m_curl,CURLOPT_ERRORBUFFER,curl_errorbuf);
    CHECK_CURL_REQ(cc);
    cc=curl_easy_setopt(m_curl,CURLOPT_NOPROGRESS,1);
    CHECK_CURL_REQ(cc);

#ifdef HAVE_CURLOPT_NOSIGNAL
    cc=curl_easy_setopt(m_curl,CURLOPT_NOSIGNAL,1);
    CHECK_CURL_REQ(cc);
    if (conf.timeout>=0)
    {
        cc=curl_easy_setopt(m_curl,CURLOPT_TIMEOUT,conf.timeout);
        CHECK_CURL_REQ(cc);
    }
    if (conf.conn_timeout>=0)
    {
        cc=curl_easy_setopt(m_curl,CURLOPT_CONNECTTIMEOUT,conf.conn_timeout);
        CHECK_CURL_REQ(cc);
    }
#endif

    cc=curl_easy_setopt(m_curl,CURLOPT_SSLVERSION,3);
    CHECK_CURL_REQ(cc);
    cc=curl_easy_setopt(m_curl,CURLOPT_SSL_VERIFYHOST,2);
    CHECK_CURL_REQ(cc);

    if (!conf.ssl_calist.empty())
    {
        cc=curl_easy_setopt(m_curl,CURLOPT_CAINFO,conf.ssl_calist.c_str());
        CHECK_CURL_REQ(cc);
    }
    
    cc=curl_easy_setopt(m_curl,CURLOPT_SSL_VERIFYPEER,conf.ssl_calist.empty() ? 0 : 1);
    CHECK_CURL_REQ(cc);

    if (!conf.ssl_certfile.empty() && !conf.ssl_keyfile.empty())
    {
        cc=curl_easy_setopt(m_curl,CURLOPT_SSLCERT,conf.ssl_certfile.c_str());
        CHECK_CURL_REQ(cc);
        if (!conf.ssl_certtype.empty())
        {
            cc=curl_easy_setopt(m_curl,CURLOPT_SSLCERTTYPE,conf.ssl_certtype.c_str());
            CHECK_CURL_REQ(cc);
        }
        cc=curl_easy_setopt(m_curl,CURLOPT_SSLKEY,conf.ssl_keyfile.c_str());
        CHECK_CURL_REQ(cc);
        cc=curl_easy_setopt(m_curl,CURLOPT_SSLKEYPASSWD,conf.ssl_keypass.c_str());
        CHECK_CURL_REQ(cc);
        if (!conf.ssl_keytype.empty())
        {
            cc=curl_easy_setopt(m_curl,CURLOPT_SSLKEYTYPE,conf.ssl_keytype.c_str());
            CHECK_CURL_REQ(cc);
        }
    }

    struct curl_slist* headers=NULL;
    headers=curl_slist_append(headers,"Content-Type: text/xml");
    cc=curl_easy_setopt(m_curl,CURLOPT_HTTPHEADER,headers);
    CHECK_CURL_REQ(cc);

    auto_ptr<char> url(XMLString::transcode(bindingInfo.getLocation()));
    cc=curl_easy_setopt(m_curl,CURLOPT_URL,url.get());
    CHECK_CURL_REQ(cc);
 
    // Make the call.
    cc=curl_easy_perform(m_curl);
    curl_slist_free_all(headers);
    CHECK_CURL_RESP(cc);

    string outbufstring=outbuf.str();
    log.debug("received from AA: %s",outbufstring.c_str());

    // Interrogate the response.
    long http_status;
    cc=curl_easy_getinfo(m_curl,CURLINFO_HTTP_CODE,&http_status);
    CHECK_CURL_RESP(cc);
    char* content_type;
    cc=curl_easy_getinfo(m_curl,CURLINFO_CONTENT_TYPE,&content_type);
    CHECK_CURL_RESP(cc);
    
    if (!content_type || !strstr(content_type,"text/xml"))
    {
        unsigned int len;
        auto_ptr<XMLByte> b(Base64::encode(const_cast<XMLByte*>((XMLByte*)outbufstring.c_str()),outbufstring.length(),&len));
        throw ContentTypeException(SAMLException::RESPONDER,reinterpret_cast<char*>(b.get()));
    }

    curl_easy_cleanup(m_curl);

    // In theory, the SOAP message should be in buf. Get a parser and build the DOM.
    XML::Parser p;
    static const XMLCh systemId[]={chLatin_A, chLatin_A, chLatin_R, chLatin_e, chLatin_s, chLatin_p, chLatin_o, chLatin_n, chLatin_s, chLatin_e, chNull};
    MemBufInputSource membufsrc(reinterpret_cast<const XMLByte*>(outbufstring.c_str()),outbufstring.length(),systemId);
    Wrapper4InputSource dsrc(&membufsrc,false);
    DOMDocument* rdoc=p.parse(dsrc);

    try
    {
        DOMElement* e=rdoc->getDocumentElement();

        // The root must be a SOAP 1.1 envelope.
        if (XMLString::compareString(XML::SOAP11ENV_NS,e->getNamespaceURI()) ||
            XMLString::compareString(L(Envelope),e->getLocalName()))
            throw MalformedException(SAMLException::RESPONDER,"SAMLSOAPBinding::send() detected an incompatible or missing SOAP envelope");

        /* Walk the children. If we encounter any headers with mustUnderstand, we have to bail.
           The thinking here is, we're not a "real" SOAP processor, but we have to emulate one that
           understands no headers. For now, assume we're the recipient.
         */
        DOMNode* n=e->getFirstChild();
        while (n && n->getNodeType()!=DOMNode::ELEMENT_NODE)
            n=n->getNextSibling();
        if (!XMLString::compareString(XML::SOAP11ENV_NS,n->getNamespaceURI()) &&
            !XMLString::compareString(L(Header),n->getLocalName()))
        {
            DOMNode* header=n->getFirstChild();
            while (header)
            {
                if (header->getNodeType()==DOMNode::ELEMENT_NODE &&
                    (static_cast<DOMElement*>(header))->getAttributeNS(XML::SOAP11ENV_NS,L(mustUnderstand)) &&
                    XMLString::parseInt((static_cast<DOMElement*>(header))->getAttributeNS(XML::SOAP11ENV_NS,L(mustUnderstand)))==1)
                    throw SOAPException(SAMLException::RESPONDER,"SAMLSOAPBinding::send() detected a mandatory SOAP header");
                header=header->getNextSibling();
            }    
            n=n->getNextSibling();   // advance to the Body element
            while (n && n->getNodeType()!=DOMNode::ELEMENT_NODE)
                n=n->getNextSibling();
        }

        if (n)
        {
            // Get the first (and only) child element of the Body.
            n=n->getFirstChild();
            while (n && n->getNodeType()!=DOMNode::ELEMENT_NODE)
                n=n->getNextSibling();
            if (n)
            {
                // Is it a fault?
                if (n->getNodeType()==DOMNode::ELEMENT_NODE &&
                    !XMLString::compareString(XML::SOAP11ENV_NS,n->getNamespaceURI()) &&
                    !XMLString::compareString(L(Fault),n->getLocalName()))
                {
                    // Find the faultstring element and use it in the message.
                    DOMNodeList* nlist=(static_cast<DOMElement*>(n))->getElementsByTagNameNS(NULL,L(faultstring));
                    if (nlist && nlist->getLength())
                    {
                        vector<saml::QName> codes;
                        auto_ptr<char> msg(XMLString::transcode(nlist->item(0)->getFirstChild()->getNodeValue()));
                        nlist=(static_cast<DOMElement*>(n))->getElementsByTagNameNS(NULL,L(faultcode));
                        if (nlist && nlist->getLength())
                        {
                            auto_ptr<saml::QName> qcode(saml::QName::getQNameTextNode(static_cast<DOMText*>(nlist->item(0)->getFirstChild())));
                            codes.push_back(*qcode);
                        }
                        throw SOAPException(Iterator<saml::QName>(codes),msg.get());
                    }
                    else
                        throw SOAPException(SAMLException::RESPONDER,"SAMLSOAPBinding::send() detected a SOAP fault");
                }

                SAMLResponse* response=new SAMLResponse(static_cast<DOMElement*>(n));
                response->setDocument(rdoc);
                return response;
            }
        }
        throw SOAPException(SAMLException::RESPONDER,"SAMLSOAPBinding::send() unable to find a SAML response or fault in SOAP body");
    }
    catch(...)
    {
        rdoc->release();
        throw;
    }
}

SAMLRequest* SAMLSOAPBinding::recieve(void* reqContext, const XMLCh** requester)
{
    throw SAMLException(SAMLException::RESPONDER,"SAMLSOAPBinding::receive() not yet implemented");
}

void SAMLSOAPBinding::respond(void* respContext, SAMLResponse* response, SAMLException* e)
{
    throw SAMLException(SAMLException::RESPONDER,"SAMLSOAPBinding::respond() not yet implemented");
}
