// $OpenLDAP$
/*
 * Copyright 2000-2020 The OpenLDAP Foundation, All Rights Reserved.
 * COPYING RESTRICTIONS APPLY, see COPYRIGHT file
 */


#include "LDAPUrl.h"
#include <sstream>
#include <iomanip>
#include "debug.h"

using namespace std;

#define PCT_ENCFLAG_NONE 0x0000U
#define PCT_ENCFLAG_COMMA 0x0001U
#define PCT_ENCFLAG_SLASH 0x0002U

#define LDAP_DEFAULT_PORT 389
#define LDAPS_DEFAULT_PORT 636

LDAPUrl::LDAPUrl(const std::string &url)
{
    DEBUG(LDAP_DEBUG_CONSTRUCT, "LDAPUrl::LDAPUrl()" << endl);
    DEBUG(LDAP_DEBUG_CONSTRUCT | LDAP_DEBUG_PARAMETER,
            "   url:" << url << endl);
    m_urlString = url;
    m_Filter = "";
    m_Scheme = "ldap";
    m_Scope = 0;
    m_Port = 0;
    regenerate = false;
    if (url != "") {
        this->parseUrl();
    }
}

LDAPUrl::~LDAPUrl()
{
    DEBUG(LDAP_DEBUG_DESTROY, "LDAPUrl::~LDAPUrl()" << endl);
    m_Attrs.clear();
}

int LDAPUrl::getPort() const 
{
    return m_Port;
}

void LDAPUrl::setPort(int port)
{
    m_Port = port;
    regenerate = true;
}

int LDAPUrl::getScope() const 
{
    return m_Scope;
}

void LDAPUrl::setScope( const std::string &scope )
{
    if (scope == "base" || scope == "" ) {
        m_Scope = 0;
    } else if (scope == "one" ) {
        m_Scope = 1;
    } else if (scope == "sub" ) {
        m_Scope = 2;
    } else {
        throw LDAPUrlException(LDAPUrlException::INVALID_SCOPE, 
                "Scope was:" + scope); 
    }
    regenerate = true;
}

const string& LDAPUrl::getURLString() const
{
    if (regenerate){
        this->components2Url();
        regenerate=false;
    }
    return m_urlString;
}

void LDAPUrl::setURLString( const std::string &url )
{
    m_urlString = url;
    if (url != "") {
        this->parseUrl();
    }
    regenerate = false;
}

const string& LDAPUrl::getHost() const 
{
    return m_Host;
}

void LDAPUrl::setHost( const std::string &host )
{
    m_Host = host;
    regenerate = true;
}

const string& LDAPUrl::getDN() const 
{
    return m_DN;
}
void LDAPUrl::setDN( const std::string &dn )
{
    m_DN = dn;
    regenerate = true;
}

const string& LDAPUrl::getFilter() const 
{
    return m_Filter;
}
void LDAPUrl::setFilter( const std::string &filter )
{
    m_Filter = filter;
    regenerate = true;
}

const StringList& LDAPUrl::getAttrs() const 
{
    return m_Attrs;
}
void LDAPUrl::setAttrs( const StringList &attrs )
{
    m_Attrs = attrs;
    regenerate = true;
}

const StringList& LDAPUrl::getExtensions() const 
{
    return m_Extensions;
}

void LDAPUrl::setExtensions( const StringList &ext )
{
    m_Extensions = ext;
    regenerate = true;
}

const std::string& LDAPUrl::getScheme() const
{
    return m_Scheme;
}

void LDAPUrl::setScheme( const std::string &scheme )
{
    if (scheme == "ldap" || scheme == "ldaps" || 
            scheme == "ldapi" || scheme == "cldap" ) 
    {
        m_Scheme = scheme;
        regenerate = true;
    } else {
        throw LDAPUrlException(LDAPUrlException::INVALID_SCHEME,
                "Unknown URL scheme: \"" + scheme + "\"");
    }
}

void LDAPUrl::parseUrl() 
{
    DEBUG(LDAP_DEBUG_TRACE, "LDAPUrl::parseUrl()" << std::endl);
    // reading Scheme
    std::string::size_type pos = m_urlString.find(':');
    std::string::size_type startpos = pos;
    if (pos == std::string::npos) {
        throw LDAPUrlException(LDAPUrlException::INVALID_URL,
                "No colon found in URL");
    }
    std::string scheme = m_urlString.substr(0, pos);
    DEBUG(LDAP_DEBUG_TRACE, "    scheme is <" << scheme << ">" << std::endl);

    if ( scheme == "ldap" ) {
        m_Scheme = scheme;
    } else if ( scheme == "ldaps" ) {
        m_Scheme = scheme;
    } else if ( scheme == "ldapi" ) {
        m_Scheme = scheme;
    } else if ( scheme == "cldap" ) {
        m_Scheme = scheme;
    } else {
        throw LDAPUrlException(LDAPUrlException::INVALID_SCHEME,
                "Unknown URL Scheme: \"" + scheme + "\"");
    }

    if ( m_urlString[pos+1] != '/' || m_urlString[pos+2] != '/' ) {
        throw LDAPUrlException(LDAPUrlException::INVALID_URL);
    } else {
        startpos = pos + 3;
    }
    if ( m_urlString[startpos] == '/' ) {
        // no hostname and port
        startpos++;
    } else {
        std::string::size_type hostend, portstart=0;
        pos = m_urlString.find('/', startpos);

        // IPv6 Address?
        if ( m_urlString[startpos] == '[' ) {
            // skip
            startpos++;
            hostend =  m_urlString.find(']', startpos);
            if ( hostend == std::string::npos ){
                throw LDAPUrlException(LDAPUrlException::INVALID_URL);
            }
            portstart = hostend + 1;
        } else {
            hostend = m_urlString.find(':', startpos);
            if ( hostend == std::string::npos || portstart > pos ) {
                hostend = pos;
            }
            portstart = hostend;
        }
        std::string host = m_urlString.substr(startpos, hostend - startpos);
        DEBUG(LDAP_DEBUG_TRACE, "    host: <" << host << ">" << std::endl);
        percentDecode(host, m_Host);

        if (portstart >= m_urlString.length() || portstart >= pos ) {
            if ( m_Scheme == "ldap" || m_Scheme == "cldap" ) {
                m_Port = LDAP_DEFAULT_PORT;
            } else if ( m_Scheme == "ldaps" ) {
                m_Port = LDAPS_DEFAULT_PORT;
            }
        } else {
            std::string port = m_urlString.substr(portstart+1, 
                    (pos == std::string::npos ? pos : pos-portstart-1) );
            if ( port.length() > 0 ) {
                std::istringstream i(port);
                i >> m_Port;
                if ( i.fail() ){
                    throw LDAPUrlException(LDAPUrlException::INVALID_PORT);
                }
            }
            DEBUG(LDAP_DEBUG_TRACE, "    Port: <" << m_Port << ">" 
                    << std::endl);
        }
        startpos = pos + 1;
    }
    int parserMode = base;
    while ( pos != std::string::npos ) {
        pos = m_urlString.find('?', startpos);
        std::string actComponent = m_urlString.substr(startpos, 
                pos - startpos);
        DEBUG(LDAP_DEBUG_TRACE, "    ParserMode:" << parserMode << std::endl);
        DEBUG(LDAP_DEBUG_TRACE, "    ActComponent: <" << actComponent << ">" 
                << std::endl);
        std::string s_scope = "";
        std::string s_ext = "";
        switch(parserMode) {
            case base :
                percentDecode(actComponent, m_DN);
                DEBUG(LDAP_DEBUG_TRACE, "    BaseDN:" << m_DN << std::endl); 
                break;
            case attrs :
                DEBUG(LDAP_DEBUG_TRACE, "    reading Attributes" << std::endl);
                if (actComponent.length() != 0 ) {
                    string2list(actComponent,m_Attrs, true);
                }
                break;
            case scope :
                percentDecode(actComponent, s_scope);
                if (s_scope == "base" || s_scope == "" ) {
                    m_Scope = 0;
                } else if (s_scope == "one" ) {
                    m_Scope = 1;
                } else if (s_scope == "sub" ) {
                    m_Scope = 2;
                } else {
                    throw LDAPUrlException(LDAPUrlException::INVALID_SCOPE);
                }
                DEBUG(LDAP_DEBUG_TRACE, "    Scope: <" << s_scope << ">"
                        << std::endl);
                break;
            case filter :
                percentDecode(actComponent, m_Filter);
                DEBUG(LDAP_DEBUG_TRACE, "    filter: <" << m_Filter << ">"
                        << std::endl);
                break;
            case extensions :
                DEBUG(LDAP_DEBUG_TRACE, "    reading Extensions" << std::endl); 
                string2list(actComponent, m_Extensions, true);
                break;
            default : 
                DEBUG(LDAP_DEBUG_TRACE, "    unknown state" << std::endl); 
                break;
        }
        startpos = pos + 1;
        parserMode++;
    }
}

void LDAPUrl::percentDecode(const std::string& src, std::string &out)
{
    DEBUG(LDAP_DEBUG_TRACE, "LDAPUrl::percentDecode()" << std::endl); 
    std::string::size_type pos = 0;
    std::string::size_type startpos = 0;
    pos = src.find('%', startpos);
    while ( pos != std::string::npos ) {
        out += src.substr(startpos, pos - startpos);
        std::string istr(src.substr(pos+1, 2));
        std::istringstream i(istr);
        i.setf(std::ios::hex, std::ios::basefield);
        i.unsetf(std::ios::showbase);
        int hex;
        i >> hex;
        if ( i.fail() ){
            throw LDAPUrlException(LDAPUrlException::URL_DECODING_ERROR, 
                    "Invalid percent encoding");
        }
        char j = hex;
        out.push_back(j);
        startpos = pos+3;
        pos = src.find('%', startpos);
    } 
    out += src.substr(startpos, pos - startpos);
}

void LDAPUrl::string2list(const std::string &src, StringList& sl, 
            bool percentDecode)
{
    std::string::size_type comma_startpos = 0;
    std::string::size_type comma_pos = 0;
    std::string actItem;
    while ( comma_pos != std::string::npos ) {
        comma_pos = src.find(',', comma_startpos);
        actItem = src.substr(comma_startpos, comma_pos - comma_startpos);
        if (percentDecode){
            std::string decoded;
            this->percentDecode(actItem,decoded);
            actItem = decoded;
        }
        sl.add(actItem);
        comma_startpos = comma_pos + 1;
    }
}


void LDAPUrl::components2Url() const
{
    std::ostringstream url; 
    std::string encoded = "";
    
    url << m_Scheme << "://";
    // IPv6 ?
    if ( m_Host.find( ':', 0 ) != std::string::npos ) {
        url <<  "[" << this->percentEncode(m_Host, encoded) <<  "]";
    } else {
        url << this->percentEncode(m_Host, encoded, PCT_ENCFLAG_SLASH);
    }

    if ( m_Port != 0 ) {
        url << ":" << m_Port;
    }

    url << "/";
    encoded = "";
    if ( m_DN != "" ) {
        this->percentEncode( m_DN, encoded );
        url << encoded;
    }
    string qm = "";
    if ( ! m_Attrs.empty() ){
        url << "?";
        bool first = true;
        for ( StringList::const_iterator i = m_Attrs.begin();
                i != m_Attrs.end(); i++) 
        {
            this->percentEncode( *i, encoded );
            if ( ! first ) {
                url << ",";
            } else {
                first = false;
            }
            url << encoded;
        }
    } else {
        qm.append("?");
    }
    if ( m_Scope == 1 ) {
        url << qm << "?one";
        qm = "";
    } else if ( m_Scope == 2 ) {
        url << qm << "?sub";
        qm = "";
    } else {
        qm.append("?");
    }
    if (m_Filter != "" ){
        this->percentEncode( m_Filter, encoded );
        url << qm << "?" << encoded;
        qm = "";
    } else {
        qm.append("?");
    }

    if ( ! m_Extensions.empty() ){
        url << qm << "?";
        bool first = true;
        for ( StringList::const_iterator i = m_Extensions.begin();
                i != m_Extensions.end(); i++) 
        {
            this->percentEncode( *i, encoded, 1);
            if ( ! first ) {
                url << ",";
            } else {
                first = false;
            }
            url << encoded;
        }
    }
    m_urlString=url.str();  
}


std::string& LDAPUrl::percentEncode( const std::string &src, 
        std::string &dest, 
        int flags) const
{
    std::ostringstream o;
    o.setf(std::ios::hex, std::ios::basefield);
    o.setf(std::ios::uppercase);
    o.unsetf(std::ios::showbase);
    bool escape=false;
    for ( std::string::const_iterator i = src.begin(); i != src.end(); i++ ){
        switch(*i){
            /* reserved */
            case '?' :
                escape = true;
            break;
            case ',' :
                if ( flags & PCT_ENCFLAG_COMMA ) {
                    escape = true;
                } else {
                    escape = false;
                }
            break;
            case ':' :
            case '/' :
                if ( flags & PCT_ENCFLAG_SLASH ) {
                    escape = true;
                } else {
                    escape = false;
                }
            break;
            case '#' :
            case '[' :
            case ']' :
            case '@' :
            case '!' :
            case '$' :
            case '&' :
            case '\'' :
            case '(' :
            case ')' :
            case '*' :
            case '+' :
            case ';' :
            case '=' :
            /* unreserved */
            case '-' :
            case '.' :
            case '_' :
            case '~' :
                escape = false;
            break;
            default :
                if (  std::isalnum(*i) ) {
                    escape = false;
                } else {
                    escape = true;
                }
            break;
        }
        if ( escape ) {
            o << "%" << std::setw(2) << std::setfill('0') << (int)(unsigned char)*i ;
        } else {
            o.put(*i);
        }
    }
    dest = o.str();
    return dest;
}

const code2string_s LDAPUrlException::code2string[] = {
   { INVALID_SCHEME,            "Invalid URL Scheme" },
   { INVALID_PORT,              "Invalid Port in Url" },
   { INVALID_SCOPE,             "Invalid Search Scope in Url" },
   { INVALID_URL,               "Invalid LDAP Url" },
   { URL_DECODING_ERROR,        "Url-decoding Error" },
   { 0, 0 }
};

LDAPUrlException::LDAPUrlException( int code, const std::string &msg) :
    m_code(code), m_addMsg(msg) {}

int LDAPUrlException::getCode() const
{
    return m_code;
}

const std::string LDAPUrlException::getAdditionalInfo() const
{
    return m_addMsg;
}

const std::string LDAPUrlException::getErrorMessage() const
{
    for ( int i = 0; code2string[i].string != 0; i++ ) {
        if ( code2string[i].code == m_code ) {
            return std::string(code2string[i].string);
        }
    }
    return "";

}
