# Authors:
#   Jason Gerard DeRose <jderose@redhat.com>
#   Rob Crittenden <rcritten@redhat.com>
#
# Copyright (C) 2008  Red Hat
# see file 'COPYING' for use and warranty 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 3 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 <http://www.gnu.org/licenses/>.

"""
RPC client and shared RPC client/server functionality.

This module adds some additional functionality on top of the ``xmlrpc.client``
module in the Python standard library (``xmlrpclib`` in Python 2).
For documentation on the ``xmlrpclib`` module, see:

    http://docs.python.org/2/library/xmlrpclib.html

Also see the `ipaserver.rpcserver` module.
"""

from __future__ import absolute_import

from decimal import Decimal
import datetime
import logging
import os
import locale
import base64
import json
import re
import socket
import gzip
from cryptography import x509 as crypto_x509

import gssapi
from dns import resolver, rdatatype
from dns.exception import DNSException
from ssl import SSLError
import six
from six.moves import urllib

from ipalib.backend import Connectible
from ipalib.constants import LDAP_GENERALIZED_TIME_FORMAT
from ipalib.errors import (public_errors, UnknownError, NetworkError,
                           XMLRPCMarshallError, JSONError)
from ipalib import errors, capabilities
from ipalib.request import context, Connection
from ipalib.x509 import Encoding as x509_Encoding
from ipapython import ipautil
from ipapython import session_storage
from ipapython.cookie import Cookie
from ipapython.dnsutil import DNSName
from ipalib.text import _
from ipalib.util import create_https_connection
from ipalib.krb_utils import KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN, KRB5KRB_AP_ERR_TKT_EXPIRED, \
                             KRB5_FCC_PERM, KRB5_FCC_NOFILE, KRB5_CC_FORMAT, \
                             KRB5_REALM_CANT_RESOLVE, KRB5_CC_NOTFOUND, get_principal
from ipapython.dn import DN
from ipapython.kerberos import Principal
from ipalib.capabilities import VERSION_WITHOUT_CAPABILITIES
from ipalib import api

# The XMLRPC client is in  "six.moves.xmlrpc_client", but pylint
# cannot handle that
try:
    from xmlrpclib import (Binary, Fault, DateTime, dumps, loads, ServerProxy,
            Transport, ProtocolError, MININT, MAXINT)
except ImportError:
    # pylint: disable=import-error
    from xmlrpc.client import (Binary, Fault, DateTime, dumps, loads, ServerProxy,
            Transport, ProtocolError, MININT, MAXINT)

# pylint: disable=import-error
if six.PY3:
    from http.client import RemoteDisconnected
else:
    from httplib import BadStatusLine as RemoteDisconnected
# pylint: enable=import-error


if six.PY3:
    unicode = str

logger = logging.getLogger(__name__)

COOKIE_NAME = 'ipa_session'
CCACHE_COOKIE_KEY = 'X-IPA-Session-Cookie'

errors_by_code = dict((e.errno, e) for e in public_errors)


def update_persistent_client_session_data(principal, data):
    '''
    Given a principal create or update the session data for that
    principal in the persistent secure storage.

    Raises ValueError if unable to perform the action for any reason.
    '''

    try:
        session_storage.store_data(principal, CCACHE_COOKIE_KEY, data)
    except Exception as e:
        raise ValueError(str(e))

def read_persistent_client_session_data(principal):
    '''
    Given a principal return the stored session data for that
    principal from the persistent secure storage.

    Raises ValueError if unable to perform the action for any reason.
    '''

    try:
        return session_storage.get_data(principal, CCACHE_COOKIE_KEY)
    except Exception as e:
        raise ValueError(str(e))

def delete_persistent_client_session_data(principal):
    '''
    Given a principal remove the session data for that
    principal from the persistent secure storage.

    Raises ValueError if unable to perform the action for any reason.
    '''

    try:
        session_storage.remove_data(principal, CCACHE_COOKIE_KEY)
    except Exception as e:
        raise ValueError(str(e))

def xml_wrap(value, version):
    """
    Wrap all ``str`` in ``xmlrpc.client.Binary``.

    Because ``xmlrpc.client.dumps()`` will itself convert all ``unicode`` instances
    into UTF-8 encoded ``str`` instances, we don't do it here.

    So in total, when encoding data for an XML-RPC packet, the following
    transformations occur:

        * All ``str`` instances are treated as binary data and are wrapped in
          an ``xmlrpc.client.Binary()`` instance.

        * Only ``unicode`` instances are treated as character data. They get
          converted to UTF-8 encoded ``str`` instances (although as mentioned,
          not by this function).

    Also see `xml_unwrap()`.

    :param value: The simple scalar or simple compound value to wrap.
    """
    if type(value) in (list, tuple):
        return tuple(xml_wrap(v, version) for v in value)
    if isinstance(value, dict):
        return dict(
            (k, xml_wrap(v, version)) for (k, v) in value.items()
        )
    if type(value) is bytes:
        return Binary(value)
    if type(value) is Decimal:
        # transfer Decimal as a string
        return unicode(value)
    if isinstance(value, six.integer_types) and (value < MININT or value > MAXINT):
        return unicode(value)
    if isinstance(value, DN):
        return str(value)

    # Encode datetime.datetim
e objects as xmlrpc.client.DateTime objects
    if isinstance(value, datetime.datetime):
        if capabilities.client_has_capability(version, 'datetime_values'):
            return DateTime(value)
        else:
            return value.strftime(LDAP_GENERALIZED_TIME_FORMAT)

    if isinstance(value, DNSName):
        if capabilities.client_has_capability(version, 'dns_name_values'):
            return {'__dns_name__': unicode(value)}
        else:
            return unicode(value
)

    if isinstance(value, Principal):
        return unicode(value)

    if isinstance(value, crypto_x509.Certificate):
        return base64.b64encode(
            value.public_bytes(x509_Encoding.DER)).decode('ascii')

    if isinstance(value, crypto_x509.CertificateSigningRequest):
        return base64.b64encode(
            value.public_bytes(x509_Encoding.DER)).decode('ascii')

    assert type(value) in (unicode, float, bool, type(None)) + six.integer_types
    return value


def xml_unwrap(value, encoding='UTF-8'):
    """
    Unwrap all ``xmlrpc.Binary``, decode all ``str`` into ``unicode``.

    When decoding data from an XML-RPC packet, the following transformations
    occur:

        * The binary payloads of all ``xmlrpc.client.Binary`` instances are
          returned as ``str`` instances.

        * All ``str`` instances are treated as UTF-8 encoded Unicode strings.
          They are decoded and the resulting ``unicode`` instance is returned.

    Also see `xml_wrap()`.

    :param value: The value to unwrap.
    :param encoding: The Unicode encoding to use (defaults to ``'UTF-8'``).
    """
    if type(value) in (list, tuple):
        return tuple(xml_unwrap(v, encoding) for v in value)
    if type(value) is dict:
        if '__dns_name__' in value:
            return DNSName(value['__dns_name__'])
        else:
            return dict(
                (k, xml_unwrap(v, encoding)) for (k, v) in value.items()
            )
    if isinstance(value, bytes):
        return value.decode(encoding)
    if isinstance(value, Binary):
        assert type(value.data) is bytes
        return value.data
    if isinstance(value, DateTime):
        # xmlprc DateTime is converted to string of %Y%m%dT%H:%M:%S format
        return datetime.datetime.strptime(str(value), "%Y%m%