/*
 *
 * 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 "sasl-internal.h"

#include "engine/engine-internal.h"

static const char ANONYMOUS[] = "ANONYMOUS";
static const char EXTERNAL[] = "EXTERNAL";
static const char PLAIN[] = "PLAIN";

bool pni_init_server(pn_transport_t* transport)
{
  return true;
}

bool pni_init_client(pn_transport_t* transport)
{
  return true;
}

void pni_sasl_impl_free(pn_transport_t *transport)
{
  free(transport->sasl->impl_context);
}

// Client handles ANONYMOUS or PLAIN mechanisms if offered
bool pni_process_mechanisms(pn_transport_t *transport, const char *mechs)
{
  // Check whether offered EXTERNAL, PLAIN or ANONYMOUS
  // Look for "EXTERNAL" in mechs
  const char *found = strstr(mechs, EXTERNAL);
  // Make sure that string is separated and terminated and allowed
  if (found && (found==mechs || found[-1]==' ') && (found[8]==0 || found[8]==' ') &&
      pni_included_mech(transport->sasl->included_mechanisms, pn_bytes(8, found))) {
    transport->sasl->selected_mechanism = pn_strdup(EXTERNAL);
    if (transport->sasl->username) {
      size_t size = strlen(transport->sasl->username);
      char *iresp = (char *) malloc(size);
      if (!iresp) return false;

      transport->sasl->impl_context = iresp;

      memmove(iresp, transport->sasl->username, size);
      transport->sasl->bytes_out.start = iresp;
      transport->sasl->bytes_out.size =  size;
    } else {
      static const char empty[] = "";
      transport->sasl->bytes_out.start = empty;
      transport->sasl->bytes_out.size =  0;
    }
    return true;
  }

  // Look for "PLAIN" in mechs
  found = strstr(mechs, PLAIN);
  // Make sure that string is separated and terminated, allowed
  // and we have a username and password and connection is encrypted or we allow insecure
  if (found && (found==mechs || found[-1]==' ') && (found[5]==0 || found[5]==' ') &&
      pni_included_mech(transport->sasl->included_mechanisms, pn_bytes(5, found)) &&
      (transport->sasl->external_ssf > 0 || transport->sasl->allow_insecure_mechs) &&
      transport->sasl->username && transport->sasl->password) {
    transport->sasl->selected_mechanism = pn_strdup(PLAIN);
    size_t usize = strlen(transport->sasl->username);
    size_t psize = strlen(transport->sasl->password);
    size_t size = usize + psize + 2;
    char *iresp = (char *) malloc(size);
    if (!iresp) return false;

    transport->sasl->impl_context = iresp;

    iresp[0] = 0;
    memmove(&iresp[1], transport->sasl->username, usize);
    iresp[usize + 1] = 0;
    memmove(&iresp[usize + 2], transport->sasl->password, psize);
    transport->sasl->bytes_out.start = iresp;
    transport->sasl->bytes_out.size =  size;

    // Zero out password and dealloc
    free(memset(transport->sasl->password, 0, psize));
    transport->sasl->password = NULL;

    return true;
  }

  // Look for "ANONYMOUS" in mechs
  found = strstr(mechs, ANONYMOUS);
  // Make sure that string is separated and terminated and allowed
  if (found && (found==mechs || found[-1]==' ') && (found[9]==0 || found[9]==' ') &&
      pni_included_mech(transport->sasl->included_mechanisms, pn_bytes(9, found))) {
    transport->sasl->selected_mechanism = pn_strdup(ANONYMOUS);
    if (transport->sasl->username) {
      size_t size = strlen(transport->sasl->username);
      char *iresp = (char *) malloc(size);
      if (!iresp) return false;

      transport->sasl->impl_context = iresp;

      memmove(iresp, transport->sasl->username, size);
      transport->sasl->bytes_out.start = iresp;
      transport->sasl->bytes_out.size =  size;
    } else {
      static const char anon[] = "anonymous";
      transport->sasl->bytes_out.start = anon;
      transport->sasl->bytes_out.size =  sizeof anon-1;
    }
    return true;
  }
  return false;
}

// Server will offer only ANONYMOUS and EXTERNAL if appropriate
int pni_sasl_impl_list_mechs(pn_transport_t *transport, char **mechlist)
{
  // If we have an external authid then we can offer EXTERNAL
  if (transport->sasl && transport->sasl->external_auth) {
    *mechlist = pn_strdup("EXTERNAL ANONYMOUS");
    return 2;
  } else {
    *mechlist = pn_strdup("ANONYMOUS");
    return 1;
  }
}

void pni_process_init(pn_transport_t *transport, const char *mechanism, const pn_bytes_t *recv)
{
  // Check that mechanism is ANONYMOUS and it is allowed
  if (strcmp(mechanism, ANONYMOUS)==0 &&
      pni_included_mech(transport->sasl->included_mechanisms, pn_bytes(sizeof(ANONYMOUS)-1, ANONYMOUS))) {
    transport->sasl->username = "anonymous";
    transport->sasl->outcome = PN_SASL_OK;
    transport->authenticated = true;
    pni_sasl_set_desired_state(transport, SASL_POSTED_OUTCOME);
  } else if (strcmp(mechanism, EXTERNAL)==0 &&
      transport->sasl->external_auth &&
      pni_included_mech(transport->sasl->included_mechanisms, pn_bytes(sizeof(EXTERNAL)-1, EXTERNAL))) {
    transport->sasl->username = transport->sasl->external_auth;
    transport->sasl->outcome = PN_SASL_OK;
    transport->authenticated = true;
    pni_sasl_set_desired_state(transport, SASL_POSTED_OUTCOME);
  } else {
    transport->sasl->outcome = PN_SASL_AUTH;
    pni_sasl_set_desired_state(transport, SASL_POSTED_OUTCOME);
  }
}

/* The default implementation neither sends nor receives challenges or responses */
void pni_process_challenge(pn_transport_t *transport, const pn_bytes_t *recv)
{
}

void pni_process_response(pn_transport_t *transport, const pn_bytes_t *recv)
{
}

bool pni_sasl_impl_can_encrypt(pn_transport_t *transport)
{
  return false;
}

ssize_t pni_sasl_impl_max_encrypt_size(pn_transport_t *transport)
{
  return 0;
}

ssize_t pni_sasl_impl_encode(pn_transport_t *transport, pn_bytes_t in, pn_bytes_t *out)
{
  return 0;
}

ssize_t pni_sasl_impl_decode(pn_transport_t *transport, pn_bytes_t in, pn_bytes_t *out)
{
  return 0;
}

bool pn_sasl_extended(void)
{
  return false;
}
