/* 
   Unix SMB/CIFS implementation.

   cldap client library

   Copyright (C) Andrew Tridgell 2005
   Copyright (C) Stefan Metzmacher 2009
   
   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/>.
*/

/*
  see RFC1798 for details of CLDAP

  basic properties
    - carried over UDP on port 389
    - request and response matched by message ID
    - request consists of only a single searchRequest element
    - response can be in one of two forms
       - a single searchResponse, followed by a searchResult
       - a single searchResult
*/

#include "includes.h"
#include <tevent.h>
#include "../lib/util/dlinklist.h"
#include "../libcli/ldap/ldap_message.h"
#include "../libcli/ldap/ldap_ndr.h"
#include "../libcli/cldap/cldap.h"
#include "../lib/tsocket/tsocket.h"
#include "../libcli/security/dom_sid.h"
#include "../librpc/gen_ndr/ndr_nbt.h"
#include "../lib/util/asn1.h"
#include "../lib/util/tevent_ntstatus.h"
#include "lib/util/idtree_random.h"

#undef strcasecmp

/*
  context structure for operations on cldap packets
*/
struct cldap_socket {
	/* the low level socket */
	struct tdgram_context *sock;

	/*
	 * Are we in connected mode, which means
	 * we get ICMP errors back instead of timing
	 * out requests. And we can only send requests
	 * to the connected peer.
	 */
	bool connected;

	/* the queue for outgoing dgrams */
	struct tevent_queue *send_queue;

	/* do we have an async tsocket_recvfrom request pending */
	struct tevent_req *recv_subreq;

	struct {
		/* a queue of pending search requests */
		struct cldap_search_state *list;

		/* mapping from message_id to pending request */
		struct idr_context *idr;
	} searches;

	/* what to do with incoming request packets */
	struct {
		struct tevent_context *ev;
		void (*handler)(struct cldap_socket *,
				void *private_data,
				struct cldap_incoming *);
		void *private_data;
	} incoming;
};

struct cldap_search_state {
	struct cldap_search_state *prev, *next;

	struct {
		struct tevent_context *ev;
		struct cldap_socket *cldap;
	} caller;

	int message_id;

	struct {
		uint32_t idx;
		uint32_t delay;
		uint32_t count;
		struct tsocket_address *dest;
		DATA_BLOB blob;
	} request;

	struct {
		struct cldap_incoming *in;
		struct asn1_data *asn1;
	} response;

	struct tevent_req *req;
};

/*
 * For CLDAP we limit the maximum search request size to 4kb
 */
#define MAX_SEARCH_REQUEST 4096

static int cldap_socket_destructor(struct cldap_socket *c)
{
	while (c->searches.list) {
		struct cldap_search_state *s = c->searches.list;
		DLIST_REMOVE(c->searches.list, s);
		ZERO_STRUCT(s->caller);
	}

	talloc_free(c->recv_subreq);
	talloc_free(c->send_queue);
	talloc_free(c->sock);
	return 0;
}

static void cldap_recvfrom_done(struct tevent_req *subreq);

static bool cldap_recvfrom_setup(struct cldap_socket *c)
{
	struct tevent_context *ev;

	if (c->recv_subreq) {
		return true;
	}

	if (!c->searches.list && !c->incoming.handler) {
		return true;
	}

	ev = c->incoming.ev;
	if (ev == NULL) {
		/* this shouldn't happen but should be protected against */
		if (c->searches.list == NULL) {
			return false;
		}
		ev = c->searches.list->caller.ev;
	}

	c->recv_subreq = tdgram_recvfrom_send(c, ev, c->sock);
	if (!c->recv_subreq) {
		return false;
	}
	tevent_req_set_callback(c->recv_subreq, cldap_recvfrom_done, c);

	return true;
}

static void cldap_recvfrom_stop(struct cldap_socket *c)
{
	if (!c->recv_subreq) {
		return;
	}

	if (c->searches.list || c->incoming.handler) {
		return;
	}

	talloc_free(c->recv_subreq);
	c->recv_subreq = NULL;
}

static bool cldap_socket_recv_dgram(struct cldap_socket *c,
				    struct cldap_incoming *in);

static void cldap_recvfrom_done(struct tevent_req *subreq)
{
	struct cldap_socket *c = tevent_req_callback_data(subreq,
				 struct cldap_socket);
	struct cldap_incoming *in = NULL;
	ssize_t ret;
	bool setup_done;

	c->recv_subreq = NULL;

	in = talloc_zero(c, struct cldap_incoming);
	if (!in) {
		goto nomem;
	}

	ret = tdgram_recvfrom_recv(subreq,
				   &in->recv_errno,
				   in,
				   &in->buf,
				   &in->src);
	talloc_free(subreq);
	subreq = NULL;
	if (ret >= 0) {
		in->len = ret;
	}
	if (ret == -1 && in->recv_errno == 0) {
		in->recv_errno = EIO;
	}

	/* this function should free or steal 'in' */
	setup_done = cldap_socket_recv_dgram(c, in);
	in = NULL;

	if (!setup_done && !cldap_recvfrom_setup(c)) {
		goto nomem;
	}

	return;

nomem:
	talloc_free(subreq);
	talloc_free(in);
}

/*
  handle recv events on a cldap socket
*/
static bool cldap_socket_recv_dgram(struct cldap_socket *c,
				    struct cldap_incoming *in)
{
	struct asn1_data *asn1;
	void *p;
	struct cldap_search_state *search;
	NTSTATUS status;
	struct ldap_request_limits limits = {
		.max_search_size = MAX_SEARCH_REQUEST
	};

	if (in->recv_errno != 0) {
		goto error;
	}

	asn1 = asn1_init(in, ASN1_MAX_TREE_DEPTH);
	if (!asn1) {
		goto nomem;
	}

	asn1_load_nocopy(asn1, in->buf, in->len);

	in->ldap_msg = talloc(in, struct ldap_message);
	if (in->ldap_msg == NULL) {
		goto nomem;
	}

	/* this initial decode is used to find the message id */
	status = ldap_decode(asn1, &limits, NULL, in->ldap_msg);
	if (!NT_STATUS_IS_OK(status)) {
		goto nterror;
	}

	/* find the pending request */
	p = idr_find(c->searches.idr, in->ldap_msg->messageid);
	if (p == NULL) {
		if (!c->incoming.handler) {
			TALLOC_FREE(in);
			return true;
		}

		/* this function should free or steal 'in' */
		c->incoming.handler(c, c->incoming.private_data, in);
		return false;
	}

	search = talloc_get_type_abort(p, struct cldap_search_state);
	search->response.in = talloc_move(search, &in);

	search->response.asn1 = asn1;

	asn1_load_nocopy(search->response.asn1,
			 search->response.in->buf, search->response.in->len);

	DLIST_REMOVE(c->searches.list, search);

	if (cldap_recvfrom_setup(c)) {
		tevent_req_done(search->req);
		return true;
	}

	/*
	 * This request was ok, just defer the notify of the caller
	 * and then just fail the next request if needed
	 */
	tevent_req_defer_callback(search->req, search->caller.ev);
	tevent_req_done(search->req);

	status = NT_STATUS_NO_MEMORY;
	/* in is NULL it this point */
	goto nterror;
nomem:
	in->recv_errno = ENOMEM;
error:
	status = map_nt_error_from_unix_common(in->recv_errno);
nterror:
	TALLOC_FREE(in);
	/* in connected mode the first pending search gets the error */
	if (!c->connected) {
		/* otherwise we just ignore the error */
		return false;
	}
	if (!c->searches.list) {
		return false;
	}
	/*
	 * We might called tevent_req_done() for a successful
	 * search before, so we better deliver the failure
	 * after the success, that is why we better also
	 * use tevent_req_defer_callback() here.
	 */
	tevent_req_defer_callback(c->searches.list->req,
				  c->searches.list->caller.ev);
	tevent_req_nterror(c->searches.list->req, status);
	return false;
}

/*
  initialise a cldap_sock
*/
NTSTATUS cldap_socket_init(TALLOC_CTX *mem_ctx,
			   const struct tsocket_address *local_addr,
			   const struct tsocket_address *remote_addr,
			   struct cldap_socket **_cldap)
{
	struct cldap_socket *c = NULL;
	struct tsocket_address *any = NULL;
	NTSTATUS status;
	int ret;
	const char *fam = NULL;

	if (local_addr == NULL && remote_addr == NULL) {
		return NT_STATUS_INVALID_PARAMETER_MIX;
	}

	if (remote_addr) {
		bool is_ipv4;
		bool is_ipv6;

		is_ipv4 = tsocket_address_is_inet(remote_addr, "ipv4");
		is_ipv6 = tsocket_address_is_inet(remote_addr, "ipv6");

		if (is_ipv4) {
			fam = "ipv4";
		} else if (is_ipv6) {
			fam = "ipv6";
		} else {
			return NT_STATUS_INVALID_ADDRESS;
		}
	}

	c = talloc_zero(mem_ctx, struct cldap_socket);
	if (!c) {
		goto nomem;
	}

	if (!local_addr) {
		/*
		 * Here we know the address family of the remote address.
		 */
		if (fam == NULL) {
			return NT_STATUS_INVALID_PARAMETER_MIX;
		}

		ret = tsocket_address_inet_from_strings(c, fam,
							NULL, 0,
							&any);
		if (ret != 0) {
			status = map_nt_error_from_unix_common(errno);
			goto nterror;
		}
		local_addr = any;
	}

	c->searches.idr = idr_init(c);
	if (!c->searches.idr) {
		goto nomem;
	}

	ret = tdgram_inet_udp_socket(local_addr, remote_addr,
				     c, &c->sock);
	if (ret != 0) {
		status = map_nt_error_from_unix_common(errno);
		goto nterror;
	}
	talloc_free(any);

	if (remote_addr) {
		c->connected = true;
	}

	c->send_queue = tevent_queue_create(c, "cldap_send_queue");
	if (!c->send_queue) {
		goto nomem;
	}

	talloc_set_destructor(c, cldap_socket_destructor);

	*_cldap = c;
	return NT_STATUS_OK;

nomem:
	status = NT_STATUS_NO_MEMORY;
nterror:
	talloc_free(c);
	return status;
}

/*
  setup a handler for incoming requests
*/
NTSTATUS cldap_set_incoming_handler(struct cldap_socket *c,
				    struct tevent_context *ev,
				    void (*handler)(struct cldap_socket *,
						    void *private_data,
						    struct cldap_incoming *),
				    void *private_data)
{
	if (c->connected) {
		return NT_STATUS_PIPE_CONNECTED;
	}

	c->incoming.ev = ev;
	c->incoming.handler = handler;
	c->incoming.private_data = private_data;

	if (!cldap_recvfrom_setup(c)) {
		ZERO_STRUCT(c->incoming);
		return NT_STATUS_NO_MEMORY;
	}

	return NT_STATUS_OK;
}

struct cldap_reply_state {
	struct tsocket_address *dest;
	DATA_BLOB blob;
};

static void cldap_reply_state_destroy(struct tevent_req *subreq);

/*
  queue a cldap reply for send
*/
NTSTATUS cldap_reply_send(struct cldap_socket *cldap, struct cldap_reply *io)
{
	struct cldap_reply_state *state = NULL;
	struct ldap_message *msg;
	DATA_BLOB blob1, blob2;
	NTSTATUS status;
	struct tevent_req *subreq;

	if (cldap->connected) {
		return NT_STATUS_PIPE_CONNECTED;
	}

	if (cldap->incoming.ev == NULL) {
		return NT_STATUS_INVALID_PIPE_STATE;
	}

	if (!io->dest) {
		return NT_STATUS_INVALID_ADDRESS;
	}

	state = talloc(cldap, struct cldap_reply_state);
	NT_STATUS_HAVE_NO_MEMORY(state);

	state->dest = tsocket_address_copy(io->dest, state);
	if (!state->dest) {
		goto nomem;
	}

	msg = talloc(state, struct ldap_message);
	if (!msg) {
		goto nomem;
	}

	msg->messageid       = io->messageid;
	msg->controls        = NULL;

	if (io->response) {
		msg->type = LDAP_TAG_SearchResultEntry;
		msg->r.SearchResultEntry = *io->response;

		if (!ldap_encode(msg, NULL, &blob1, state)) {
			status = NT_STATUS_INVALID_PARAMETER;
			goto failed;
		}
	} else {
		blob1 = data_blob(NULL, 0);
	}

	msg->type = LDAP_TAG_SearchResultDone;
	msg->r.SearchResultDone = *io->result;

	if (!ldap_encode(msg, NULL, &blob2, state)) {
		status = NT_STATUS_INVALID_PARAMETER;
		goto failed;
	}
	talloc_free(msg);

	state->blob = data_blob_talloc(state, NULL, blob1.length + blob2.length);
	if (!state->blob.data) {
		goto nomem;
	}

	if (blob1.data != NULL) {
		memcpy(state->blob.data, blob1.data, blob1.length);
	}
	memcpy(state->blob.data+blob1.length, blob2.data, blob2.length);
	data_blob_free(&blob1);
	data_blob_free(&blob2);

	subreq = tdgram_sendto_queue_send(state,
					  cldap->incoming.ev,
					  cldap->sock,
					  cldap->send_queue,
					  state->blob.data,
					  state->blob.length,
					  state->dest);
	if (!subreq) {
		goto nomem;
	}
	/* the callback will just free the state, as we don't need a result */
	tevent_req_set_callback(subreq, cldap_reply_state_destroy, state);

	return NT_STATUS_OK;

nomem:
	status = NT_STATUS_NO_MEMORY;
failed:
	talloc_free(state);
	return status;
}

static void cldap_reply_state_destroy(struct tevent_req *subreq)
{
	struct cldap_reply_state *state = tevent_req_callback_data(subreq,
					  struct cldap_reply_state);

	/* we don't want to know the result here, we just free the state */
	talloc_free(subreq);
	talloc_free(state);
}

static int cldap_search_state_destructor(struct cldap_search_state *s)
{
	if (s->caller.cldap) {
		if (s->message_id != -1) {
			idr_remove(s->caller.cldap->searches.idr, s->message_id);
			s->message_id = -1;
		}
		DLIST_REMOVE(s->caller.cldap->searches.list, s);
		cldap_recvfrom_stop(s->caller.cldap);
		ZERO_STRUCT(s->caller);
	}

	return 0;
}

static void cldap_search_state_queue_done(struct tevent_req *subreq);
static void cldap_search_state_wakeup_done(struct tevent_req *subreq);

/*
  queue a cldap reply for send
*/
struct tevent_req *cldap_search_send(TALLOC_CTX *mem_ctx,
				     struct tevent_context *ev,
				     struct cldap_socket *cldap,
				     const struct cldap_search *io)
{
	struct tevent_req *req, *subreq;
	struct cldap_search_state *state = NULL;
	struct ldap_message *msg;
	struct ldap_SearchRequest *search;
	struct timeval now;
	struct timeval end;
	uint32_t i;
	int ret;

	req = tevent_req_create(mem_ctx, &state,
				struct cldap_search_state);
	if (!req) {
		return NULL;
	}
	state->caller.ev = ev;
	state->req = req;
	state->caller.cldap = cldap;
	state->message_id = -1;

	talloc_set_destructor(state, cldap_search_state_destructor);

	if (state->caller.cldap == NULL) {
		tevent_req_nterror(req, NT_STATUS_INVALID_PARAMETER);
		goto post;
	}

	if (io->in.dest_address) {
		if (cldap->connected) {
			tevent_req_nterror(req, NT_STATUS_PIPE_CONNECTED);
			goto post;
		}
		ret = tsocket_address_inet_from_strings(state,
						        "ip",
						        io->in.dest_address,
						        io->in.dest_port,
							&state->request.dest);
		if (ret != 0) {
			tevent_req_nterror(req, NT_STATUS_INVALID_PARAMETER);
			goto post;
		}
	} else {
		if (!cldap->connected) {
			tevent_req_nterror(req, NT_STATUS_INVALID_ADDRESS);
			goto post;
		}
		state->request.dest = NULL;
	}

	state->message_id = idr_get_new_random(
		cldap->searches.idr, state, 1, UINT16_MAX);
	if (state->message_id == -1) {
		tevent_req_nterror(req, NT_STATUS_INSUFFICIENT_RESOURCES);
		goto post;
	}

	msg = talloc(state, struct ldap_message);
	if (tevent_req_nomem(msg, req)) {
		goto post;
	}

	msg->messageid	= state->message_id;
	msg->type	= LDAP_TAG_SearchRequest;
	msg->controls	= NULL;
	search = &msg->r.SearchRequest;

	search->basedn		= "";
	search->scope		= LDAP_SEARCH_SCOPE_BASE;
	search->deref		= LDAP_DEREFERENCE_NEVER;
	search->timelimit	= 0;
	search->sizelimit	= 0;
	search->attributesonly	= false;
	search->num_attributes	= str_list_length(io->in.attributes);
	search->attributes	= io->in.attributes;
	search->tree		= ldb_parse_tree(msg, io->in.filter);
	if (tevent_req_nomem(search->tree, req)) {
		goto post;
	}

	if (!ldap_encode(msg, NULL, &state->request.blob, state)) {
		tevent_req_nterror(req, NT_STATUS_INVALID_PARAMETER);
		goto post;
	}
	talloc_free(msg);

	state->request.idx = 0;
	state->request.delay = 10*1000*1000;
	state->request.count = 3;
	if (io->in.timeout > 0) {
		state->request.delay = io->in.timeout * 1000 * 1000;
		state->request.count = io->in.retries + 1;
	}

	now = tevent_timeval_current();
	end = now;
	for (i = 0; i < state->request.count; i++) {
		end = tevent_timeval_add(&end, state->request.delay / 1000000,
					 state->request.delay % 1000000);
	}

	if (!tevent_req_set_endtime(req, state->caller.ev, end)) {
		goto post;
	}

	subreq = tdgram_sendto_queue_send(state,
					  state->caller.ev,
					  state->caller.cldap->sock,
					  state->caller.cldap->send_queue,
					  state->request.blob.data,
					  state->request.blob.length,
					  state->request.dest);
	if (tevent_req_nomem(subreq, req)) {
		goto post;
	}
	tevent_req_set_callback(subreq, cldap_search_state_queue_done, req);

	DLIST_ADD_END(cldap->searches.list, state);

	return req;

 post:
	return tevent_req_post(req, state->caller.ev);
}

static void cldap_search_state_queue_done(struct tevent_req *subreq)
{
	struct tevent_req *req = tevent_req_callback_data(subreq,
				 struct tevent_req);
	struct cldap_search_state *state = tevent_req_data(req,
					   struct cldap_search_state);
	ssize_t ret;
	int sys_errno = 0;
	struct timeval next;

	ret = tdgram_sendto_queue_recv(subreq, &sys_errno);
	talloc_free(subreq);
	if (ret == -1) {
		NTSTATUS status;
		status = map_nt_error_from_unix_common(sys_errno);
		DLIST_REMOVE(state->caller.cldap->searches.list, state);
		ZERO_STRUCT(state->caller.cldap);
		tevent_req_nterror(req, status);
		return;
	}

	state->request.idx++;

	/* wait for incoming traffic */
	if (!cldap_recvfrom_setup(state->caller.cldap)) {
		tevent_req_oom(req);
		return;
	}

	if (state->request.idx > state->request.count) {
		/* we just wait for the response or a timeout */
		return;
	}

	next = tevent_timeval_current_ofs(state->request.delay / 1000000,
					  state->request.delay % 1000000);
	subreq = tevent_wakeup_send(state,
				    state->caller.ev,
				    next);
	if (tevent_req_nomem(subreq, req)) {
		return;
	}
	tevent_req_set_callback(subreq, cldap_search_state_wakeup_done, req);
}

static void cldap_search_state_wakeup_done(struct tevent_req *subreq)
{
	struct tevent_req *req = tevent_req_callback_data(subreq,
				 struct tevent_req);
	struct cldap_search_state *state = tevent_req_data(req,
					   struct cldap_search_state);
	bool ok;

	ok = tevent_wakeup_recv(subreq);
	talloc_free(subreq);
	if (!ok) {
		tevent_req_nterror(req, NT_STATUS_INTERNAL_ERROR);
		return;
	}

	subreq = tdgram_sendto_queue_send(state,
					  state->caller.ev,
					  state->caller.cldap->sock,
					  state->caller.cldap->send_queue,
					  state->request.blob.data,
					  state->request.blob.length,
					  state->request.dest);
	if (tevent_req_nomem(subreq, req)) {
		return;
	}
	tevent_req_set_callback(subreq, cldap_search_state_queue_done, req);
}

/*
  receive a cldap reply
*/
NTSTATUS cldap_search_recv(struct tevent_req *req,
			   TALLOC_CTX *mem_ctx,
			   struct cldap_search *io)
{
	struct cldap_search_state *state = tevent_req_data(req,
					   struct cldap_search_state);
	struct ldap_message *ldap_msg;
	NTSTATUS status;
	struct ldap_request_limits limits = {
		.max_search_size = MAX_SEARCH_REQUEST
	};

	if (tevent_req_is_nterror(req, &status)) {
		goto failed;
	}

	ldap_msg = talloc(mem_ctx, struct ldap_message);
	if (!ldap_msg) {
		goto nomem;
	}

	status = ldap_decode(state->response.asn1, &limits, NULL, ldap_msg);
	if (!NT_STATUS_IS_OK(status)) {
		goto failed;
	}

	ZERO_STRUCT(io->out);

	/* the first possible form has a search result in first place */
	if (ldap_msg->type == LDAP_TAG_SearchResultEntry) {
		io->out.response = talloc(mem_ctx, struct ldap_SearchResEntry);
		if (!io->out.response) {
			goto nomem;
		}
		*io->out.response = ldap_msg->r.SearchResultEntry;

		/* decode the 2nd part */
		status = ldap_decode(
			state->response.asn1, &limits, NULL, ldap_msg);
		if (!NT_STATUS_IS_OK(status)) {
			goto failed;
		}
	}

	if (ldap_msg->type != LDAP_TAG_SearchResultDone) {
		status = NT_STATUS_LDAP(LDAP_PROTOCOL_ERROR);
		goto failed;
	}

	io->out.result = talloc(mem_ctx, struct ldap_Result);
	if (!io->out.result) {
		goto nomem;
	}
	*io->out.result = ldap_msg->r.SearchResultDone;

	if (io->out.result->resultcode != LDAP_SUCCESS) {
		status = NT_STATUS_LDAP(io->out.result->resultcode);
		goto failed;
	}

	tevent_req_received(req);
	return NT_STATUS_OK;

nomem:
	status = NT_STATUS_NO_MEMORY;
failed:
	tevent_req_received(req);
	return status;
}


/*
  synchronous cldap search
*/
NTSTATUS cldap_search(struct cldap_socket *cldap,
		      TALLOC_CTX *mem_ctx,
		      struct cldap_search *io)
{
	TALLOC_CTX *frame;
	struct tevent_req *req;
	struct tevent_context *ev;
	NTSTATUS status;

	if (cldap->searches.list) {
		return NT_STATUS_PIPE_BUSY;
	}

	if (cldap->incoming.handler) {
		return NT_STATUS_INVALID_PIPE_STATE;
	}

	frame = talloc_stackframe();

	ev = samba_tevent_context_init(frame);
	if (ev == NULL) {
		TALLOC_FREE(frame);
		return NT_STATUS_NO_MEMORY;
	}

	req = cldap_search_send(mem_ctx, ev, cldap, io);
	if (req == NULL) {
		TALLOC_FREE(frame);
		return NT_STATUS_NO_MEMORY;
	}

	if (!tevent_req_poll(req, ev)) {
		status = map_nt_error_from_unix_common(errno);
		TALLOC_FREE(frame);
		return status;
	}

	status = cldap_search_recv(req, mem_ctx, io);
	if (!NT_STATUS_IS_OK(status)) {
		TALLOC_FREE(frame);
		return status;
	}

	TALLOC_FREE(frame);
	return NT_STATUS_OK;
}
