GSSAPI="BASE"  # This ensures that a full module is generated by Cython

from libc.stdlib cimport free

from gssapi.raw.cython_types cimport *
from gssapi.raw.cython_converters cimport c_py_ttl_to_c, c_c_ttl_to_py
from gssapi.raw.creds cimport Creds
from gssapi.raw.names cimport Name
from gssapi.raw.oids cimport OID
from gssapi.raw.chan_bindings cimport ChannelBindings

from gssapi.raw.types import MechType, RequirementFlag, IntEnumFlagSet
from gssapi.raw.misc import GSSError
from gssapi.raw.named_tuples import AcceptSecContextResult
from gssapi.raw.named_tuples import InitSecContextResult
from gssapi.raw.named_tuples import InquireContextResult


cdef extern from "python_gssapi.h":
    OM_uint32 gss_init_sec_context(OM_uint32 *min_stat,
                                   const gss_cred_id_t initiator_creds,
                                   gss_ctx_id_t *context,
                                   const gss_name_t target_name,
                                   const gss_OID mech_type,
                                   OM_uint32 flags,
                                   OM_uint32 ttl,
                                   const gss_channel_bindings_t chan_bdgs,
                                   const gss_buffer_t input_token,
                                   gss_OID *actual_mech_type,
                                   gss_buffer_t output_token,
                                   OM_uint32 *actual_flags,
                                   OM_uint32 *actual_ttl) nogil

    OM_uint32 gss_accept_sec_context(OM_uint32 *min_stat,
                                     gss_ctx_id_t *context,
                                     const gss_cred_id_t acceptor_creds,
                                     const gss_buffer_t input_token,
                                     const gss_channel_bindings_t chan_bdgs,
                                     const gss_name_t *initiator_name,
                                     gss_OID *mech_type,
                                     gss_buffer_t output_token,
                                     OM_uint32 *flags,
                                     OM_uint32 *ttl,
                                     gss_cred_id_t *delegated_creds) nogil

    OM_uint32 gss_delete_sec_context(OM_uint32 *min_stat,
                                     gss_ctx_id_t *context,
                                     gss_buffer_t output_token) nogil

    OM_uint32 gss_process_context_token(OM_uint32 *min_stat,
                                        const gss_ctx_id_t context,
                                        const gss_buffer_t token) nogil

    OM_uint32 gss_context_time(OM_uint32 *min_stat,
                               const gss_ctx_id_t context_handle,
                               OM_uint32 *ttl) nogil

    OM_uint32 gss_inquire_context(OM_uint32 *min_stat,
                                  const gss_ctx_id_t context,
                                  gss_name_t *initiator_name,
                                  gss_name_t *target_name,
                                  OM_uint32 *ttl,
                                  gss_OID *mech_type,
                                  OM_uint32 *ctx_flags,
                                  int *locally_initiated,
                                  int *is_open) nogil

    OM_uint32 gss_export_sec_context(OM_uint32 *min_stat,
                                     gss_ctx_id_t *context,
                                     gss_buffer_t interprocess_token) nogil

    OM_uint32 gss_import_sec_context(OM_uint32 *min_stat,
                                     const gss_buffer_t interprocess_token,
                                     gss_ctx_id_t *context) nogil


cdef class SecurityContext:
    """
    A GSSAPI Security Context
    """
    # defined in pxd
    # cdef gss_ctx_id_t raw_ctx

    def __cinit__(self, SecurityContext cpy=None):
        if cpy is not None:
            self.raw_ctx = cpy.raw_ctx
            cpy.raw_ctx = GSS_C_NO_CONTEXT
        else:
            self.raw_ctx = GSS_C_NO_CONTEXT

    property _started:
        """Whether the underlying context is NULL."""

        def __get__(self):
            return self.raw_ctx is not NULL

    def __dealloc__(self):
        # basically just deleteSecContext, but we are not
        # allowed to call methods here
        cdef OM_uint32 maj_stat, min_stat
        if self.raw_ctx is not GSS_C_NO_CONTEXT:
            # local deletion only
            maj_stat = gss_delete_sec_context(&min_stat, &self.raw_ctx,
                                              GSS_C_NO_BUFFER)
            if maj_stat != GSS_S_COMPLETE:
                raise GSSError(maj_stat, min_stat)

            self.raw_ctx = NULL


# TODO(directxman12): figure out whether GSS_C_NO_NAME can be passed in here
def init_sec_context(Name target_name not None, Creds creds=None,
                     SecurityContext context=None,
                     OID mech=None,
                     flags=None, lifetime=None,
                     ChannelBindings channel_bindings=None,
                     input_token=None):
    """
    init_sec_context(target_name, creds=None, context=None, mech=None, \
flags=None, lifetime=None, channel_bindings=None, input_token=None)
    Initiate a GSSAPI security context.

    This method initiates a GSSAPI security context, targeting the given
    target name.  To create a basic context, just provide the target name.
    Further calls used to update the context should pass in the output context
    of the last call, as well as the input token received from the acceptor.

    Warning:
        This changes the input context!

    Args:
        target_name (Name): the target for the security context
        creds (Creds): the credentials to use to initiate the context,
            or None to use the default credentials
        context (SecurityContext): the security context to update, or
            None to create a new context
        mech (MechType): the mechanism type for this security context,
            or None for the default mechanism type
        flags (list): the flags to request for the security context, or
            None to use the default set: mutual_authentication and
            out_of_sequence_detection.  This may also be an
            :class:`IntEnumFlagSet`
        lifetime (int): the request lifetime of the security context (a value
            of 0 or None means indefinite)
        channel_bindings (ChannelBindings): The channel bindings (or None for
            no channel bindings)
        input_token (bytes): the token to use to update the security context,
            or None if you are creating a new context

    Returns:
        InitSecContextResult: the output security context, the actual mech
        type, the actual flags used, the output token to send to the acceptor,
        the actual lifetime of the context (or None if not supported or
        indefinite), and whether or not more calls are needed to finish the
        initiation.

    Raises:
        InvalidTokenError
        InvalidCredentialsError
        MissingCredentialsError
        ExpiredCredentialsError
        BadChannelBindingsError
        BadMICError
        ExpiredTokenError
        DuplicateTokenError
        MissingContextError
        BadNameTypeError
        BadNameError
        BadMechanismError
    """

    cdef gss_OID mech_oid
    if mech is not None:
        mech_oid = &mech.raw_oid
    else:
        mech_oid = GSS_C_NO_OID

    # TODO(directxman12): should we default to this?
    cdef OM_uint32 req_flags = IntEnumFlagSet(RequirementFlag, flags or [
        RequirementFlag.mutual_authentication,
        RequirementFlag.out_of_sequence_detection])

    cdef gss_channel_bindings_t bdng
    if channel_bindings is not None:
        bdng = channel_bindings.__cvalue__()
    else:
        bdng = GSS_C_NO_CHANNEL_BINDINGS

    cdef gss_buffer_desc input_token_buffer = gss_buffer_desc(0, NULL)

    cdef OM_uint32 input_ttl = c_py_ttl_to_c(lifetime)

    cdef SecurityContext output_context = context
    if output_context is None:
        output_context = SecurityContext()

    cdef gss_cred_id_t act_cred
    if creds is not None:
        act_cred = creds.raw_creds
    else:
        act_cred = GSS_C_NO_CREDENTIAL

    if input_token is not None:
        input_token_buffer.value = input_token
        input_token_buffer.length = len(input_token)

    cdef gss_OID actual_mech_type = GSS_C_NO_OID
    cdef gss_buffer_desc output_token_buffer = gss_buffer_desc(0, NULL)
    cdef OM_uint32 ret_flags
    cdef OM_uint32 output_ttl

    cdef OM_uint32 maj_stat, min_stat

    with nogil:
        maj_stat = gss_init_sec_context(&min_stat, act_cred,
                                        &output_context.raw_ctx,
                                        target_name.raw_name,
                                        mech_oid, req_flags, input_ttl,
                                        bdng, &input_token_buffer,
                                        &actual_mech_type,
                                        &output_token_buffer,
                                        &ret_flags, &output_ttl)

    output_token = None
    if output_token_buffer.length:
        output_token = \
            (<char*>output_token_buffer.value)[:output_token_buffer.length]
    cdef OM_uint32 tmp_min_stat
    gss_release_buffer(&tmp_min_stat, &output_token_buffer)

    if channel_bindings is not None:
        free(bdng)

    cdef OID output_mech_type = OID()
    if maj_stat == GSS_S_COMPLETE or maj_stat == GSS_S_CONTINUE_NEEDED:
        if actual_mech_type is not GSS_C_NO_OID:
            output_mech_type.raw_oid = actual_mech_type[0]

        return InitSecContextResult(output_context, output_mech_type,
                                    IntEnumFlagSet(RequirementFlag, ret_flags),
                                    output_token,
                                    c_c_ttl_to_py(output_ttl),
                                    maj_stat == GSS_S_CONTINUE_NEEDED)
    else:
        raise GSSError(maj_stat, min_stat, token=output_token)


def accept_sec_context(input_token not None, Creds acceptor_creds=None,
                       SecurityContext context=None,
                       ChannelBindings channel_bindings=None):
    """
    accept_sec_context(input_token, acceptor_creds=None, context=None, \
channel_bindings=None)
    Accept a GSSAPI security context.

    This method accepts a GSSAPI security context using a token sent by the
    initiator, using the given credentials.  It can either be used to accept a
    security context and create a new security context object, or to update an
    existing security context object.

    Warning:
        This changes the input context!

    Args:
        input_token (bytes): the token sent by the context initiator
        acceptor_creds (Creds): the credentials to be used to accept the
            context (or None to use the default credentials)
        context (SecurityContext): the security context to update
            (or None to create a new security context object)
        channel_bindings (ChannelBindings): The channel bindings (or None for
            no channel bindings)

    Returns:
        AcceptSecContextResult: the resulting security context, the initiator
            name, the mechanism being used, the output token, the flags in use,
            the lifetime of the context (or None for indefinite or not
            supported), the delegated credentials (valid only if the
            delegate_to_peer flag is set), and whether or not further token
            exchanges are needed to finalize the security context.

    Raises:
        InvalidTokenError
        InvalidCredentialsError
        MissingCredentialsError
        ExpiredCredentialsError
        BadChannelBindingsError
        MissingContextError
        BadMICError
        ExpiredTokenError
        DuplicateTokenError
        BadMechanismError
    """

    cdef gss_channel_bindings_t bdng
    if channel_bindings is not None:
        bdng = channel_bindings.__cvalue__()
    else:
        bdng = GSS_C_NO_CHANNEL_BINDINGS

    cdef gss_buffer_desc input_token_buffer = gss_buffer_desc(len(input_token),
                                                              input_token)

    cdef SecurityContext output_context = context
    if output_context is None:
        output_context = SecurityContext()

    cdef gss_cred_id_t act_acceptor_cred
    if acceptor_creds is not None:
        act_acceptor_cred = acceptor_creds.raw_creds
    else:
        act_acceptor_cred = GSS_C_NO_CREDENTIAL

    cdef gss_name_t initiator_name
    cdef gss_OID mech_type
    # GSS_C_EMPTY_BUFFER
    cdef gss_buffer_desc output_token_buffer = gss_buffer_desc(0, NULL)
    cdef OM_uint32 ret_flags
    cdef OM_uint32 output_ttl
    cdef gss_cred_id_t delegated_cred

    cdef OM_uint32 maj_stat, min_stat

    with nogil:
        maj_stat = gss_accept_sec_context(&min_stat, &output_context.raw_ctx,
                                          act_acceptor_cred,
                                          &input_token_buffer, bdng,
                                          &initiator_name,
                                          &mech_type, &output_token_buffer,
                                          &ret_flags, &output_ttl,
                                          &delegated_cred)

    output_token = None
    if output_token_buffer.length:
        output_token = \
            (<char*>output_token_buffer.value)[:output_token_buffer.length]
    cdef OM_uint32 tmp_min_stat
    gss_release_buffer(&tmp_min_stat, &output_token_buffer)

    if channel_bindings is not None:
        free(bdng)

    cdef Name on = Name()
    cdef Creds oc = None
    cdef OID py_mech_type
    if maj_stat == GSS_S_COMPLETE or maj_stat == GSS_S_CONTINUE_NEEDED:
        if output_ttl == GSS_C_INDEFINITE:
            output_ttl_py = None
        else:
            output_ttl_py = output_ttl

        on.raw_name = initiator_name

        if delegated_cred is not NULL:
            oc = Creds()
            oc.raw_creds = delegated_cred

        if mech_type is not NULL:
            py_mech_type = OID()
            py_mech_type.raw_oid = mech_type[0]
        else:
            py_mech_type = None

        return AcceptSecContextResult(output_context, on, py_mech_type,
                                      output_token,
                                      IntEnumFlagSet(RequirementFlag,
                                                     ret_flags),
                                      output_ttl_py, oc,
                                      maj_stat == GSS_S_CONTINUE_NEEDED)
    else:
        raise GSSError(maj_stat, min_stat, token=output_token)


def inquire_context(SecurityContext context not None, initiator_name=True,
                    target_name=True, lifetime=True, mech=True,
                    flags=True, locally_init=True, complete=True):
    """
    inquire_context(context, initiator_name=True, target_name=True, \
lifetime=True, mech=True, flags=True, locally_init=True, complete=True)
    Get information about a security context.

    This method obtains information about a security context, including
    the initiator and target names, as well as the TTL, mech,
    flags, and its current state (open vs closed).

    Note:
        the target name may be ``None`` if it would have been ``GSS_C_NO_NAME``

    Args:
        context (SecurityContext): the context in question

    Returns:
        InquireContextResult: the initiator name, the target name, the TTL
            (can be None for indefinite or not supported), the mech type, the
            flags, whether or not the context was locally initiated,
            and whether or not the context is currently fully established

    Raises:
        MissingContextError
    """

    cdef gss_name_t output_init_name
    cdef gss_name_t *init_name_ptr = NULL
    if initiator_name:
        init_name_ptr = &output_init_name

    cdef gss_name_t output_target_name
    cdef gss_name_t *target_name_ptr = NULL
    if target_name:
        target_name_ptr = &output_target_name

    cdef OM_uint32 ttl
    cdef OM_uint32 *ttl_ptr = NULL
    if lifetime:
        ttl_ptr = &ttl

    cdef gss_OID output_mech_type
    cdef gss_OID *mech_type_ptr = NULL
    if mech:
        mech_type_ptr = &output_mech_type

    cdef OM_uint32 output_flags
    cdef OM_uint32 *flags_ptr = NULL
    if flags:
        flags_ptr = &output_flags

    cdef int output_locally_init
    cdef int *locally_init_ptr = NULL
    if locally_init:
        locally_init_ptr = &output_locally_init

    cdef int is_complete
    cdef int *is_complete_ptr = NULL
    if complete:
        is_complete_ptr = &is_complete

    cdef OM_uint32 maj_stat, min_stat

    maj_stat = gss_inquire_context(&min_stat, context.raw_ctx, init_name_ptr,
                                   target_name_ptr, ttl_ptr, mech_type_ptr,
                                   flags_ptr, locally_init_ptr,
                                   is_complete_ptr)

    cdef Name sn
    cdef OID py_mech_type
    cdef Name tn
    if maj_stat == GSS_S_COMPLETE:
        if initiator_name:
            sn = Name()
            sn.raw_name = output_init_name
        else:
            sn = None

        if target_name and output_target_name != GSS_C_NO_NAME:
            tn = Name()
            tn.raw_name = output_target_name
        else:
            tn = None

        if mech:
            py_mech_type = OID()
            py_mech_type.raw_oid = output_mech_type[0]
        else:
            py_mech_type = None

        if lifetime and ttl != GSS_C_INDEFINITE:
            py_ttl = ttl
        else:
            py_ttl = None

        if flags:
            py_flags = IntEnumFlagSet(RequirementFlag, output_flags)
        else:
            py_flags = None

        if locally_init:
            py_locally_init = <bint>output_locally_init
        else:
            py_locally_init = None

        if complete:
            py_complete = <bint>is_complete
        else:
            py_complete = None

        return InquireContextResult(sn, tn, py_ttl, py_mech_type, py_flags,
                                    py_locally_init, py_complete)
    else:
        raise GSSError(maj_stat, min_stat)


def context_time(SecurityContext context not None):
    """
    context_time(context)
    Get the amount of time for which the given context will remain valid.

    This method determines the amount of time for which the given
    security context will remain valid.  An expired context will
    give a result of 0.

    Args:
        context (SecurityContext): the security context in question

    Returns:
        int: the number of seconds for which the context will be valid

    Raises:
        ExpiredContextError
        MissingContextError
    """

    cdef OM_uint32 ttl

    cdef OM_uint32 maj_stat, min_stat

    maj_stat = gss_context_time(&min_stat, context.raw_ctx, &ttl)

    if maj_stat == GSS_S_COMPLETE:
        return ttl
    else:
        raise GSSError(maj_stat, min_stat)


def process_context_token(SecurityContext context not None, token):
    """
    process_context_token(context, token)
    Process a token asynchronously.

    This method provides a way to process a token, even if the
    given security context is not expecting one.  For example,
    if the initiator has the initSecContext return that the context
    is complete, but the acceptor is unable to accept the context,
    and wishes to send a token to the initiator, letting the
    initiator know of the error.

    Warning:
        This method has been essentially deprecated by :rfc:`2744`.

    Args:
        context (SecurityContext): the security context against which
            to process the token
        token (bytes): the token to process

    Raises:
        InvalidTokenError
        MissingContextError
    """

    cdef gss_buffer_desc token_buffer = gss_buffer_desc(len(token), token)

    cdef OM_uint32 maj_stat, min_stat

    with nogil:
        maj_stat = gss_process_context_token(&min_stat, context.raw_ctx,
                                             &token_buffer)

    if maj_stat != GSS_S_COMPLETE:
        raise GSSError(maj_stat, min_stat)


def import_sec_context(token not None):
    """
    import_sec_context(token)
    Import a context from another process.

    This method imports a security context established in another process
    by reading the specified token which was output by
    :func:`export_sec_context`.

    Raises:
        MissingContextError
        InvalidTokenError
        OperationUnavailableError
        UnauthorizedError
    """

    cdef gss_buffer_desc token_buffer = gss_buffer_desc(len(token), token)

    cdef gss_ctx_id_t ctx

    cdef OM_uint32 maj_stat, min_stat

    with nogil:
        maj_stat = gss_import_sec_context(&min_stat, &token_buffer, &ctx)

    if maj_stat == GSS_S_COMPLETE:
        res = SecurityContext()
        res.raw_ctx = ctx
        return res
    else:
        raise GSSError(maj_stat, min_stat)


def export_sec_context(SecurityContext context not None):
    """
    export_sec_context(context)
    Export a context for use in another process.

    This method exports a security context, deactivating in the current process
    and creating a token which can then be imported into another process
    with :func:`import_sec_context`.

    Warning: this modifies the input context

    Args:
        context (SecurityContext): the context to send to another process

    Returns:
        bytes: the output token to be imported

    Raises:
        ExpiredContextError
        MissingContextError
        OperationUnavailableError
    """

    cdef gss_buffer_desc output_token = gss_buffer_desc(0, NULL)

    cdef OM_uint32 maj_stat, min_stat

    with nogil:
        maj_stat = gss_export_sec_context(&min_stat, &context.raw_ctx,
                                          &output_token)

    if maj_stat == GSS_S_COMPLETE:
        res_token = (<char*>output_token.value)[:output_token.length]
        gss_release_buffer(&min_stat, &output_token)
        return res_token
    else:
        raise GSSError(maj_stat, min_stat)


def delete_sec_context(SecurityContext context not None, local_only=True):
    """
    delete_sec_context(context, local_only=True)
    Delete a GSSAPI security context.

    This method deletes a GSSAPI security context,
    returning an output token to send to the other
    holder of the security context to notify them
    of the deletion.

    Note:
        This method generally should not be used.  :class:`SecurityContext`
        objects will automatically be freed by Python.

    Args:
        context (SecurityContext): the security context in question
        local_only (bool): should we request local deletion (True), or also
            remote deletion (False), in which case a token is also returned

    Returns:
        bytes: the output token (if remote deletion is requested).  Generally
            this is None, but bytes for compatibility.

    Raises:
        MissingContextError
    """

    cdef OM_uint32 maj_stat, min_stat
    # GSS_C_EMPTY_BUFFER
    cdef gss_buffer_desc output_token = gss_buffer_desc(0, NULL)
    if not local_only:
        maj_stat = gss_delete_sec_context(&min_stat, &context.raw_ctx,
                                          &output_token)
    else:
        maj_stat = gss_delete_sec_context(&min_stat, &context.raw_ctx,
                                          GSS_C_NO_BUFFER)

    if maj_stat == GSS_S_COMPLETE:
        res = (<char*>output_token.value)[:output_token.length]
        context.raw_ctx = NULL
        return res
    else:
        raise GSSError(maj_stat, min_stat)
