/* ldap.cc: Helper functions for ldap access to Active Directory.

   Copyright 2014 Red Hat, Inc.

This file is part of Cygwin.

This software is a copyrighted work licensed under the terms of the
Cygwin license.  Please consult the file "CYGWIN_LICENSE" for
details. */

#include "winsup.h"
#include "ldap.h"
#include "cygerrno.h"
#include "security.h"
#include "path.h"
#include "fhandler.h"
#include "dtable.h"
#include "cygheap.h"
#include "registry.h"
#include "pinfo.h"
#include "lm.h"
#include "dsgetdc.h"
#include "tls_pbuf.h"

static LDAP_TIMEVAL tv = { 3, 0 };

static PWCHAR rootdse_attr[] =
{
  (PWCHAR) L"defaultNamingContext",
  (PWCHAR) L"supportedCapabilities",
  NULL
};

static PWCHAR user_attr[] =
{
  (PWCHAR) L"uid",
  (PWCHAR) L"primaryGroupID",
  (PWCHAR) L"gecos",
  (PWCHAR) L"unixHomeDirectory",
  (PWCHAR) L"loginShell",
  (PWCHAR) L"uidNumber",
  NULL
};

static PWCHAR group_attr[] =
{
  (PWCHAR) L"cn",
  (PWCHAR) L"gidNumber",
  NULL
};

PWCHAR tdom_attr[] =
{
  (PWCHAR) L"trustPosixOffset",
  NULL
};

PWCHAR sid_attr[] =
{
  (PWCHAR) L"objectSid",
  NULL
};

PWCHAR rfc2307_uid_attr[] =
{
  (PWCHAR) L"uid",
  NULL
};

PWCHAR rfc2307_gid_attr[] =
{
  (PWCHAR) L"cn",
  NULL
};

DWORD WINAPI
rediscover_thread (LPVOID domain)
{
  PDOMAIN_CONTROLLER_INFOW pdci;
  DWORD ret = DsGetDcNameW (NULL, (PWCHAR) domain, NULL, NULL,
			    DS_FORCE_REDISCOVERY | DS_ONLY_LDAP_NEEDED, &pdci);
  if (ret == ERROR_SUCCESS)
    NetApiBufferFree (pdci);
  else
    debug_printf ("DsGetDcNameW(%W) failed with error %u", domain, ret);
  return 0;
}

bool
cyg_ldap::connect_ssl (PCWSTR domain)
{
  ULONG ret, timelimit = 3; /* secs */

  if (!(lh = ldap_sslinitW ((PWCHAR) domain, LDAP_SSL_PORT, 1)))
    {
      debug_printf ("ldap_init(%W) error 0x%02x", domain, LdapGetLastError ());
      return false;
    }
  if ((ret = ldap_set_option (lh, LDAP_OPT_TIMELIMIT, &timelimit))
      != LDAP_SUCCESS)
    debug_printf ("ldap_set_option(LDAP_OPT_TIMELIMIT) error 0x%02x", ret);
  if ((ret = ldap_bind_s (lh, NULL, NULL, LDAP_AUTH_NEGOTIATE)) != LDAP_SUCCESS)
    {
      debug_printf ("ldap_bind(%W) 0x%02x", domain, ret);
      ldap_unbind (lh);
      lh = NULL;
      return false;
    }
  return true;
}

bool
cyg_ldap::connect_non_ssl (PCWSTR domain)
{
  ULONG ret, timelimit = 3; /* secs */

  if (!(lh = ldap_initW ((PWCHAR) domain, LDAP_PORT)))
    {
      debug_printf ("ldap_init(%W) error 0x%02x", domain, LdapGetLastError ());
      return false;
    }
  if ((ret = ldap_set_option (lh, LDAP_OPT_SIGN, LDAP_OPT_ON))
      != LDAP_SUCCESS)
    debug_printf ("ldap_set_option(LDAP_OPT_SIGN) error 0x%02x", ret);
  if ((ret = ldap_set_option (lh, LDAP_OPT_ENCRYPT, LDAP_OPT_ON))
      != LDAP_SUCCESS)
    debug_printf ("ldap_set_option(LDAP_OPT_ENCRYPT) error 0x%02x", ret);
  if ((ret = ldap_set_option (lh, LDAP_OPT_TIMELIMIT, &timelimit))
      != LDAP_SUCCESS)
    debug_printf ("ldap_set_option(LDAP_OPT_TIMELIMIT) error 0x%02x", ret);
  if ((ret = ldap_bind_s (lh, NULL, NULL, LDAP_AUTH_NEGOTIATE)) != LDAP_SUCCESS)
    {
      debug_printf ("ldap_bind(%W) 0x%02x", domain, ret);
      ldap_unbind (lh);
      lh = NULL;
      return false;
    }
  return true;
}

bool
cyg_ldap::open (PCWSTR domain)
{
  LARGE_INTEGER start, stop;
  static LARGE_INTEGER last_rediscover;
  ULONG ret;

  close ();
  GetSystemTimeAsFileTime ((LPFILETIME) &start);
  /* FIXME?  connect_ssl can take ages even when failing, so we're trying to
     do everything the non-SSL (but still encrypted) way. */
  if (/*!connect_ssl (NULL) && */ !connect_non_ssl (domain))
    return false;
  /* For some obscure reason, there's a chance that the ldap_bind_s call takes
     a long time, if the current primary DC is... well, burping or something.
     If so, we rediscover in the background which usually switches to the next
     fastest DC. */
  GetSystemTimeAsFileTime ((LPFILETIME) &stop);
  if ((stop.QuadPart - start.QuadPart) >= 3000000LL		   /* 0.3s */
      && (stop.QuadPart - last_rediscover.QuadPart) >= 30000000LL) /* 3s */
    {
      debug_printf ("ldap_bind_s is laming.  Try to rediscover.");
      HANDLE thr = CreateThread (&sec_none_nih, 4 * PTHREAD_STACK_MIN,
				 rediscover_thread, (LPVOID) domain,
				 STACK_SIZE_PARAM_IS_A_RESERVATION, NULL);
      if (!thr)
	debug_printf ("Couldn't start rediscover thread.");
      else
	{
	  last_rediscover = stop;
	  CloseHandle (thr);
	}
    }

  if ((ret = ldap_search_stW (lh, NULL, LDAP_SCOPE_BASE,
			      (PWCHAR) L"(objectclass=*)", rootdse_attr,
			      0, &tv, &msg))
      != LDAP_SUCCESS)
    {
      debug_printf ("ldap_search(%W, ROOTDSE) error 0x%02x", domain, ret);
      goto err;
    }
  if (!(entry = ldap_first_entry (lh, msg)))
    {
      debug_printf ("No ROOTDSE entry for %W", domain);
      goto err;
    }
  if (!(val = ldap_get_valuesW (lh, entry, rootdse_attr[0])))
    {
      debug_printf ("No ROOTDSE value for %W", domain);
      goto err;
    }
  if (!(rootdse = wcsdup (val[0])))
    {
      debug_printf ("wcsdup(%W, ROOTDSE) %d", domain, get_errno ());
      goto err;
    }
  ldap_value_freeW (val);
  if ((val = ldap_get_valuesW (lh, entry, rootdse_attr[1])))
    {
      for (ULONG idx = 0; idx < ldap_count_valuesW (val); ++idx)
	if (!wcscmp (val[idx], LDAP_CAP_ACTIVE_DIRECTORY_OID_W))
	  {
	    isAD = true;
	    break;
	  }
    }
  ldap_value_freeW (val);
  val = NULL;
  ldap_memfreeW ((PWCHAR) msg);
  msg = entry = NULL; return true;
err:
  close ();
  return false;
}

void
cyg_ldap::close ()
{
  if (msg_id != (ULONG) -1)
    ldap_abandon (lh, msg_id);
  if (lh)
    ldap_unbind (lh);
  if (msg)
    ldap_memfreeW ((PWCHAR) msg);
  if (val)
    ldap_value_freeW (val);
  if (rootdse)
    free (rootdse);
  lh = NULL;
  msg = entry = NULL;
  val = NULL;
  rootdse = NULL;
  msg_id = (ULONG) -1;
}

bool
cyg_ldap::fetch_ad_account (PSID sid, bool group)
{
  WCHAR filter[140], *f;
  LONG len = (LONG) RtlLengthSid (sid);
  PBYTE s = (PBYTE) sid;
  static WCHAR hex_wchars[] = L"0123456789abcdef";
  ULONG ret;

  if (msg)
    {
      ldap_memfreeW ((PWCHAR) msg);
      msg = entry = NULL;
    }
  if (val)
    {
      ldap_value_freeW (val);
      val = NULL;
    }
  f = wcpcpy (filter, L"(objectSid=");
  while (len-- > 0)
    {
      *f++ = L'\\';
      *f++ = hex_wchars[*s >> 4];
      *f++ = hex_wchars[*s++ & 0xf];
    }
  wcpcpy (f, L")");
  attr = group ? group_attr : user_attr;
  if ((ret = ldap_search_stW (lh, rootdse, LDAP_SCOPE_SUBTREE, filter,
			      attr, 0, &tv, &msg)) != LDAP_SUCCESS)
    {
      debug_printf ("ldap_search_stW(%W,%W) error 0x%02x",
		    rootdse, filter, ret);
      return false;
    }
  if (!(entry = ldap_first_entry (lh, msg)))
    {
      debug_printf ("No entry for %W in rootdse %W", filter, rootdse);
      return false;
    }
  return true;
}

bool
cyg_ldap::enumerate_ad_accounts (PCWSTR domain, bool group)
{
  tmp_pathbuf tp;
  PCWSTR filter;
  PWCHAR dse;

  if (msg)
    {
      ldap_memfreeW ((PWCHAR) msg);
      msg = entry = NULL;
    }
  if (val)
    {
      ldap_value_freeW (val);
      val = NULL;
    }
  if (!group)
    filter = L"(&(objectClass=User)"
	        "(objectCategory=Person)"
		/* 512 == ADS_UF_NORMAL_ACCOUNT */
	        "(userAccountControl:" LDAP_MATCHING_RULE_BIT_AND ":=512)"
	        "(objectSid=*))";
  else if (!domain)
    filter = L"(&(objectClass=Group)"
		"(objectSid=*))";
  else
    filter = L"(&(objectClass=Group)"
		/* 1 == ACCOUNT_GROUP */
		"(!(groupType:" LDAP_MATCHING_RULE_BIT_AND ":=1))"
		"(objectSid=*))";
  if (!domain)
    dse = rootdse;
  else
    {
      /* create rootdse from domain name. */
      dse = tp.w_get ();
      PCWSTR ps, pe;
      PWCHAR d;

      d = dse;
      for (ps = domain; (pe = wcschr (ps, L'.')); ps = pe + 1)
	{
	  if (d > dse)
	    d = wcpcpy (d, L",");
	  d = wcpncpy (wcpcpy (d, L"DC="), ps, pe - ps);
	}
      if (d > dse)
	d = wcpcpy (d, L",");
      d = wcpcpy (wcpcpy (d, L"DC="), ps);
    }
  msg_id = ldap_searchW (lh, dse, LDAP_SCOPE_SUBTREE, (PWCHAR) filter,
			 sid_attr, 0);
  if (msg_id == (ULONG) -1)
    {
      debug_printf ("ldap_searchW(%W,%W) error 0x%02x", dse, filter,
		    LdapGetLastError ());
      return false;
    }
  return true;
}

bool
cyg_ldap::next_account (cygsid &sid)
{
  ULONG ret;
  PLDAP_BERVAL *bval;

  if (msg)
    {
      ldap_memfreeW ((PWCHAR) msg);
      msg = entry = NULL;
    }
  if (val)
    {
      ldap_value_freeW (val);
      val = NULL;
    }
  ret = ldap_result (lh, msg_id, LDAP_MSG_ONE, &tv, &msg);
  if (ret == 0)
    {
      debug_printf ("ldap_result() timeout!");
      return false;
    }
  if (ret == (ULONG) -1)
    {
      debug_printf ("ldap_result() error 0x%02x", LdapGetLastError ());
      return false;
    }
  if ((entry = ldap_first_entry (lh, msg))
      && (bval = ldap_get_values_lenW (lh, entry, sid_attr[0])))
    {
      sid = (PSID) bval[0]->bv_val;
      ldap_value_free_len (bval);
      return true;
    }
  return false;
}

uint32_t
cyg_ldap::fetch_posix_offset_for_domain (PCWSTR domain)
{
  WCHAR filter[300];
  ULONG ret;

  if (msg)
    {
      ldap_memfreeW ((PWCHAR) msg);
      msg = entry = NULL;
    }
  if (val)
    {
      ldap_value_freeW (val);
      val = NULL;
    }
  /* If domain name has no dot, it's a Netbios name.  In that case, filter
     by flatName rather than by name. */
  __small_swprintf (filter, L"(&(objectClass=trustedDomain)(%W=%W))",
		    wcschr (domain, L'.') ? L"name" : L"flatName", domain);
  if ((ret = ldap_search_stW (lh, rootdse, LDAP_SCOPE_SUBTREE, filter,
			      attr = tdom_attr, 0, &tv, &msg)) != LDAP_SUCCESS)
    {
      debug_printf ("ldap_search_stW(%W,%W) error 0x%02x",
		    rootdse, filter, ret);
      return 0;
    }
  if (!(entry = ldap_first_entry (lh, msg)))
    {
      debug_printf ("No entry for %W in rootdse %W", filter, rootdse);
      return 0;
    }
  return get_num_attribute (0);
}

PWCHAR
cyg_ldap::get_string_attribute (int idx)
{
  if (val)
    ldap_value_freeW (val);
  val = ldap_get_valuesW (lh, entry, attr[idx]);
  if (val)
    return val[0];
  return NULL;
}

uint32_t
cyg_ldap::get_num_attribute (int idx)
{
  PWCHAR ret = get_string_attribute (idx);
  if (ret)
    return (uint32_t) wcstoul (ret, NULL, 10);
  return (uint32_t) -1;
}

bool
cyg_ldap::fetch_unix_sid_from_ad (uint32_t id, cygsid &sid, bool group)
{
  WCHAR filter[48];
  ULONG ret;
  PLDAP_BERVAL *bval;

  if (msg)
    {
      ldap_memfreeW ((PWCHAR) msg);
      msg = entry = NULL;
    }
  if (group)
    __small_swprintf (filter, L"(&(objectClass=Group)(gidNumber=%u))", id);
  else
    __small_swprintf (filter, L"(&(objectClass=User)(uidNumber=%u))", id);
  if ((ret = ldap_search_stW (lh, rootdse, LDAP_SCOPE_SUBTREE, filter,
			      sid_attr, 0, &tv, &msg)) != LDAP_SUCCESS)
    {
      debug_printf ("ldap_search_stW(%W,%W) error 0x%02x",
		    rootdse, filter, ret);
      return false;
    }
  if ((entry = ldap_first_entry (lh, msg))
      && (bval = ldap_get_values_lenW (lh, entry, sid_attr[0])))
    {
      sid = (PSID) bval[0]->bv_val;
      ldap_value_free_len (bval);
      return true;
    }
  return false;
}

PWCHAR
cyg_ldap::fetch_unix_name_from_rfc2307 (uint32_t id, bool group)
{
  WCHAR filter[52];
  ULONG ret;

  if (msg)
    {
      ldap_memfreeW ((PWCHAR) msg);
      msg = entry = NULL;
    }
  if (val)
    {
      ldap_value_freeW (val);
      val = NULL;
    }
  attr = group ? rfc2307_gid_attr : rfc2307_uid_attr;
  if (group)
    __small_swprintf (filter, L"(&(objectClass=posixGroup)(gidNumber=%u))", id);
  else
    __small_swprintf (filter, L"(&(objectClass=posixAccount)(uidNumber=%u))",
		      id);
  if ((ret = ldap_search_stW (lh, rootdse, LDAP_SCOPE_SUBTREE, filter, attr,
			      0, &tv, &msg)) != LDAP_SUCCESS)
    {
      debug_printf ("ldap_search_stW(%W,%W) error 0x%02x",
		    rootdse, filter, ret);
      return NULL;
    }
  if (!(entry = ldap_first_entry (lh, msg)))
    {
      debug_printf ("No entry for %W in rootdse %W", filter, rootdse);
      return NULL;
    }
  return get_string_attribute (0);
}

uid_t
cyg_ldap::remap_uid (uid_t uid)
{
  cygsid user (NO_SID);
  PWCHAR name;
  struct passwd *pw;

  if (isAD)
    {
      if (fetch_unix_sid_from_ad (uid, user, false)
	  && user != NO_SID
	  && (pw = internal_getpwsid (user)))
	return pw->pw_uid;
    }
  else if ((name = fetch_unix_name_from_rfc2307 (uid, false)))
    {
      char *mbname = NULL;
      sys_wcstombs_alloc (&mbname, HEAP_NOTHEAP, name);
      if ((pw = internal_getpwnam (mbname)))
	return pw->pw_uid;
    }
  return ILLEGAL_UID;
}

gid_t
cyg_ldap::remap_gid (gid_t gid)
{
  cygsid group (NO_SID);
  PWCHAR name;
  struct group *gr;

  if (isAD)
    {
      if (fetch_unix_sid_from_ad (gid, group, true)
	  && group != NO_SID
	  && (gr = internal_getgrsid (group)))
	return gr->gr_gid;
    }
  else if ((name = fetch_unix_name_from_rfc2307 (gid, true)))
    {
      char *mbname = NULL;
      sys_wcstombs_alloc (&mbname, HEAP_NOTHEAP, name);
      if ((gr = internal_getgrnam (mbname)))
	return gr->gr_gid;
    }
  return ILLEGAL_GID;
}