/* 
   Unix SMB/CIFS implementation.

   SMB2 client request handling

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

#include "includes.h"
#include "libcli/raw/libcliraw.h"
#include "libcli/smb2/smb2.h"
#include "lib/util/dlinklist.h"
#include "lib/events/events.h"
#include "libcli/smb2/smb2_calls.h"
#include "param/param.h"

/* fill in the bufinfo */
void smb2_setup_bufinfo(struct smb2_request *req)
{
	req->in.bufinfo.mem_ctx    = req;
	req->in.bufinfo.flags      = BUFINFO_FLAG_UNICODE | BUFINFO_FLAG_SMB2;
	req->in.bufinfo.align_base = req->in.buffer;
	if (req->in.dynamic) {
		req->in.bufinfo.data       = req->in.dynamic;
		req->in.bufinfo.data_size  = req->in.body_size - req->in.body_fixed;
	} else {
		req->in.bufinfo.data       = NULL;
		req->in.bufinfo.data_size  = 0;
	}
}


/* destroy a request structure */
static int smb2_request_destructor(struct smb2_request *req)
{
	if (req->transport) {
		/* remove it from the list of pending requests (a null op if
		   its not in the list) */
		DLIST_REMOVE(req->transport->pending_recv, req);
	}
	return 0;
}

/*
  initialise a smb2 request
*/
struct smb2_request *smb2_request_init(struct smb2_transport *transport, uint16_t opcode,
				       uint16_t body_fixed_size, bool body_dynamic_present,
				       uint32_t body_dynamic_size)
{
	struct smb2_request *req;
	uint64_t seqnum;

	if (body_dynamic_present) {
		if (body_dynamic_size == 0) {
			body_dynamic_size = 1;
		}
	} else {
		body_dynamic_size = 0;
	}

	req = talloc(transport, struct smb2_request);
	if (req == NULL) return NULL;

	seqnum = transport->seqnum++;
	if (seqnum == UINT64_MAX) {
		seqnum = transport->seqnum++;
	}

	req->state     = SMB2_REQUEST_INIT;
	req->transport = transport;
	req->session   = NULL;
	req->tree      = NULL;
	req->seqnum    = seqnum;
	req->status    = NT_STATUS_OK;
	req->async.fn  = NULL;
	req->next = req->prev = NULL;

	ZERO_STRUCT(req->cancel);
	ZERO_STRUCT(req->in);

	req->out.size      = SMB2_HDR_BODY+NBT_HDR_SIZE+body_fixed_size;

	req->out.allocated = req->out.size + body_dynamic_size;
	req->out.buffer    = talloc_array(req, uint8_t, req->out.allocated);
	if (req->out.buffer == NULL) {
		talloc_free(req);
		return NULL;
	}

	req->out.hdr       = req->out.buffer + NBT_HDR_SIZE;
	req->out.body      = req->out.hdr + SMB2_HDR_BODY;
	req->out.body_fixed= body_fixed_size;
	req->out.body_size = body_fixed_size;
	req->out.dynamic   = (body_dynamic_size ? req->out.body + body_fixed_size : NULL);

	SIVAL(req->out.hdr, 0,				SMB2_MAGIC);
	SSVAL(req->out.hdr, SMB2_HDR_LENGTH,		SMB2_HDR_BODY);
	SSVAL(req->out.hdr, SMB2_HDR_EPOCH,		0);
	SIVAL(req->out.hdr, SMB2_HDR_STATUS,		0);
	SSVAL(req->out.hdr, SMB2_HDR_OPCODE,		opcode);
	SSVAL(req->out.hdr, SMB2_HDR_CREDIT,		0);
	SIVAL(req->out.hdr, SMB2_HDR_FLAGS,		0);
	SIVAL(req->out.hdr, SMB2_HDR_NEXT_COMMAND,	0);
	SBVAL(req->out.hdr, SMB2_HDR_MESSAGE_ID,		req->seqnum);
	SIVAL(req->out.hdr, SMB2_HDR_PID,		0);
	SIVAL(req->out.hdr, SMB2_HDR_TID,		0);
	SBVAL(req->out.hdr, SMB2_HDR_SESSION_ID,		0);
	memset(req->out.hdr+SMB2_HDR_SIGNATURE, 0, 16);

	/* set the length of the fixed body part and +1 if there's a dynamic part also */
	SSVAL(req->out.body, 0, body_fixed_size + (body_dynamic_size?1:0));

	/* 
	 * if we have a dynamic part, make sure the first byte
	 * which is always be part of the packet is initialized
	 */
	if (body_dynamic_size) {
		req->out.size += 1;
		SCVAL(req->out.dynamic, 0, 0);
	}

	talloc_set_destructor(req, smb2_request_destructor);

	return req;
}

/*
    initialise a smb2 request for tree operations
*/
struct smb2_request *smb2_request_init_tree(struct smb2_tree *tree, uint16_t opcode,
					    uint16_t body_fixed_size, bool body_dynamic_present,
					    uint32_t body_dynamic_size)
{
	struct smb2_request *req = smb2_request_init(tree->session->transport, opcode, 
						     body_fixed_size, body_dynamic_present,
						     body_dynamic_size);
	if (req == NULL) return NULL;

	SBVAL(req->out.hdr,  SMB2_HDR_SESSION_ID, tree->session->uid);
	SIVAL(req->out.hdr,  SMB2_HDR_TID, tree->tid);
	req->session = tree->session;
	req->tree = tree;

	return req;	
}

/* destroy a request structure and return final status */
NTSTATUS smb2_request_destroy(struct smb2_request *req)
{
	NTSTATUS status;

	/* this is the error code we give the application for when a
	   _send() call fails completely */
	if (!req) return NT_STATUS_UNSUCCESSFUL;

	if (req->state == SMB2_REQUEST_ERROR &&
	    NT_STATUS_IS_OK(req->status)) {
		status = NT_STATUS_INTERNAL_ERROR;
	} else {
		status = req->status;
	}

	talloc_free(req);
	return status;
}

/*
  receive a response to a packet
*/
bool smb2_request_receive(struct smb2_request *req)
{
	/* req can be NULL when a send has failed. This eliminates lots of NULL
	   checks in each module */
	if (!req) return false;

	/* keep receiving packets until this one is replied to */
	while (req->state <= SMB2_REQUEST_RECV) {
		if (event_loop_once(req->transport->socket->event.ctx) != 0) {
			return false;
		}
	}

	return req->state == SMB2_REQUEST_DONE;
}

/* Return true if the last packet was in error */
bool smb2_request_is_error(struct smb2_request *req)
{
	return NT_STATUS_IS_ERR(req->status);
}

/* Return true if the last packet was OK */
bool smb2_request_is_ok(struct smb2_request *req)
{
	return NT_STATUS_IS_OK(req->status);
}

/*
  check if a range in the reply body is out of bounds
*/
bool smb2_oob(struct smb2_request_buffer *buf, const uint8_t *ptr, size_t size)
{
	if (size == 0) {
		/* zero bytes is never out of range */
		return false;
	}
	/* be careful with wraparound! */
	if ((uintptr_t)ptr < (uintptr_t)buf->body ||
	    (uintptr_t)ptr >= (uintptr_t)buf->body + buf->body_size ||
	    size > buf->body_size ||
	    (uintptr_t)ptr + size > (uintptr_t)buf->body + buf->body_size) {
		return true;
	}
	return false;
}

size_t smb2_padding_size(uint32_t offset, size_t n)
{
	if ((offset & (n-1)) == 0) return 0;
	return n - (offset & (n-1));
}

static size_t smb2_padding_fix(struct smb2_request_buffer *buf)
{
	if (buf->dynamic == (buf->body + buf->body_fixed)) {
		return 1;
	}
	return 0;
}

/*
  grow a SMB2 buffer by the specified amount
*/
static NTSTATUS smb2_grow_buffer(struct smb2_request_buffer *buf, size_t increase)
{
	size_t dynamic_ofs;
	uint8_t *buffer_ptr;
	uint32_t newsize = buf->size + increase;

	/* a packet size should be limited a bit */
	if (newsize >= 0x00FFFFFF) return NT_STATUS_MARSHALL_OVERFLOW;

	if (newsize <= buf->allocated) return NT_STATUS_OK;

	dynamic_ofs = buf->dynamic - buf->buffer;

	buffer_ptr = talloc_realloc(buf, buf->buffer, uint8_t, newsize);
	NT_STATUS_HAVE_NO_MEMORY(buffer_ptr);

	buf->buffer	= buffer_ptr;
	buf->hdr	= buf->buffer + NBT_HDR_SIZE;
	buf->body	= buf->hdr    + SMB2_HDR_BODY;
	buf->dynamic	= buf->buffer + dynamic_ofs;
	buf->allocated	= newsize;

	return NT_STATUS_OK;
}

/*
  pull a uint16_t ofs/ uint16_t length/blob triple from a data blob
  the ptr points to the start of the offset/length pair
*/
NTSTATUS smb2_pull_o16s16_blob(struct smb2_request_buffer *buf, TALLOC_CTX *mem_ctx, uint8_t *ptr, DATA_BLOB *blob)
{
	uint16_t ofs, size;
	if (smb2_oob(buf, ptr, 4)) {
		return NT_STATUS_BUFFER_TOO_SMALL;
	}
	ofs  = SVAL(ptr, 0);
	size = SVAL(ptr, 2);
	if (ofs == 0) {
		*blob = data_blob(NULL, 0);
		return NT_STATUS_OK;
	}
	if (smb2_oob(buf, buf->hdr + ofs, size)) {
		return NT_STATUS_BUFFER_TOO_SMALL;
	}
	*blob = data_blob_talloc(mem_ctx, buf->hdr + ofs, size);
	NT_STATUS_HAVE_NO_MEMORY(blob->data);
	return NT_STATUS_OK;
}

/*
  push a uint16_t ofs/ uint16_t length/blob triple into a data blob
  the ofs points to the start of the offset/length pair, and is relative
  to the body start
*/
NTSTATUS smb2_push_o16s16_blob(struct smb2_request_buffer *buf, 
			       uint16_t ofs, DATA_BLOB blob)
{
	NTSTATUS status;
	size_t offset;
	size_t padding_length;
	size_t padding_fix;
	uint8_t *ptr = buf->body+ofs;

	if (buf->dynamic == NULL) {
		return NT_STATUS_INVALID_PARAMETER;
	}

	/* we have only 16 bit for the size */
	if (blob.length > 0xFFFF) {
		return NT_STATUS_BUFFER_TOO_SMALL;
	}

	/* check if there're enough room for ofs and size */
	if (smb2_oob(buf, ptr, 4)) {
		return NT_STATUS_BUFFER_TOO_SMALL;
	}

	if (blob.data == NULL) {
		if (blob.length != 0) {
			return NT_STATUS_INTERNAL_ERROR;
		}
		SSVAL(ptr, 0, 0);
		SSVAL(ptr, 2, 0);
		return NT_STATUS_OK;
	}

	offset = buf->dynamic - buf->hdr;
	padding_length = smb2_padding_size(offset, 2);
	offset += padding_length;
	padding_fix = smb2_padding_fix(buf);

	SSVAL(ptr, 0, offset);
	SSVAL(ptr, 2, blob.length);

	status = smb2_grow_buffer(buf, blob.length + padding_length - padding_fix);
	NT_STATUS_NOT_OK_RETURN(status);

	memset(buf->dynamic, 0, padding_length);
	buf->dynamic += padding_length;

	memcpy(buf->dynamic, blob.data, blob.length);
	buf->dynamic += blob.length;

	buf->size += blob.length + padding_length - padding_fix;
	buf->body_size += blob.length + padding_length;

	return NT_STATUS_OK;
}


/*
  push a uint16_t ofs/ uint32_t length/blob triple into a data blob
  the ofs points to the start of the offset/length pair, and is relative
  to the body start
*/
NTSTATUS smb2_push_o16s32_blob(struct smb2_request_buffer *buf, 
			       uint16_t ofs, DATA_BLOB blob)
{
	NTSTATUS status;
	size_t offset;
	size_t padding_length;
	size_t padding_fix;
	uint8_t *ptr = buf->body+ofs;

	if (buf->dynamic == NULL) {
		return NT_STATUS_INVALID_PARAMETER;
	}

	/* check if there're enough room for ofs and size */
	if (smb2_oob(buf, ptr, 6)) {
		return NT_STATUS_BUFFER_TOO_SMALL;
	}

	if (blob.data == NULL) {
		if (blob.length != 0) {
			return NT_STATUS_INTERNAL_ERROR;
		}
		SSVAL(ptr, 0, 0);
		SIVAL(ptr, 2, 0);
		return NT_STATUS_OK;
	}

	offset = buf->dynamic - buf->hdr;
	padding_length = smb2_padding_size(offset, 2);
	offset += padding_length;
	padding_fix = smb2_padding_fix(buf);

	SSVAL(ptr, 0, offset);
	SIVAL(ptr, 2, blob.length);

	status = smb2_grow_buffer(buf, blob.length + padding_length - padding_fix);
	NT_STATUS_NOT_OK_RETURN(status);

	memset(buf->dynamic, 0, padding_length);
	buf->dynamic += padding_length;

	memcpy(buf->dynamic, blob.data, blob.length);
	buf->dynamic += blob.length;

	buf->size += blob.length + padding_length - padding_fix;
	buf->body_size += blob.length + padding_length;

	return NT_STATUS_OK;
}


/*
  push a uint32_t ofs/ uint32_t length/blob triple into a data blob
  the ofs points to the start of the offset/length pair, and is relative
  to the body start
*/
NTSTATUS smb2_push_o32s32_blob(struct smb2_request_buffer *buf, 
			       uint32_t ofs, DATA_BLOB blob)
{
	NTSTATUS status;
	size_t offset;
	size_t padding_length;
	size_t padding_fix;
	uint8_t *ptr = buf->body+ofs;

	if (buf->dynamic == NULL) {
		return NT_STATUS_INVALID_PARAMETER;
	}

	/* check if there're enough room for ofs and size */
	if (smb2_oob(buf, ptr, 8)) {
		return NT_STATUS_BUFFER_TOO_SMALL;
	}

	if (blob.data == NULL) {
		if (blob.length != 0) {
			return NT_STATUS_INTERNAL_ERROR;
		}
		SIVAL(ptr, 0, 0);
		SIVAL(ptr, 4, 0);
		return NT_STATUS_OK;
	}

	offset = buf->dynamic - buf->hdr;
	padding_length = smb2_padding_size(offset, 8);
	offset += padding_length;
	padding_fix = smb2_padding_fix(buf);

	SIVAL(ptr, 0, offset);
	SIVAL(ptr, 4, blob.length);

	status = smb2_grow_buffer(buf, blob.length + padding_length - padding_fix);
	NT_STATUS_NOT_OK_RETURN(status);

	memset(buf->dynamic, 0, padding_length);
	buf->dynamic += padding_length;

	memcpy(buf->dynamic, blob.data, blob.length);
	buf->dynamic += blob.length;

	buf->size += blob.length + padding_length - padding_fix;
	buf->body_size += blob.length + padding_length;

	return NT_STATUS_OK;
}


/*
  push a uint32_t length/ uint32_t ofs/blob triple into a data blob
  the ofs points to the start of the length/offset pair, and is relative
  to the body start
*/
NTSTATUS smb2_push_s32o32_blob(struct smb2_request_buffer *buf, 
			       uint32_t ofs, DATA_BLOB blob)
{
	NTSTATUS status;
	size_t offset;
	size_t padding_length;
	size_t padding_fix;
	uint8_t *ptr = buf->body+ofs;

	if (buf->dynamic == NULL) {
		return NT_STATUS_INVALID_PARAMETER;
	}

	/* check if there're enough room for ofs and size */
	if (smb2_oob(buf, ptr, 8)) {
		return NT_STATUS_BUFFER_TOO_SMALL;
	}

	if (blob.data == NULL) {
		if (blob.length != 0) {
			return NT_STATUS_INTERNAL_ERROR;
		}
		SIVAL(ptr, 0, 0);
		SIVAL(ptr, 4, 0);
		return NT_STATUS_OK;
	}

	offset = buf->dynamic - buf->hdr;
	padding_length = smb2_padding_size(offset, 8);
	offset += padding_length;
	padding_fix = smb2_padding_fix(buf);

	SIVAL(ptr, 0, blob.length);
	SIVAL(ptr, 4, offset);

	status = smb2_grow_buffer(buf, blob.length + padding_length - padding_fix);
	NT_STATUS_NOT_OK_RETURN(status);

	memset(buf->dynamic, 0, padding_length);
	buf->dynamic += padding_length;

	memcpy(buf->dynamic, blob.data, blob.length);
	buf->dynamic += blob.length;

	buf->size += blob.length + padding_length - padding_fix;
	buf->body_size += blob.length + padding_length;

	return NT_STATUS_OK;
}

/*
  pull a uint16_t ofs/ uint32_t length/blob triple from a data blob
  the ptr points to the start of the offset/length pair
*/
NTSTATUS smb2_pull_o16s32_blob(struct smb2_request_buffer *buf, TALLOC_CTX *mem_ctx, uint8_t *ptr, DATA_BLOB *blob)
{
	uint16_t ofs;
	uint32_t size;

	if (smb2_oob(buf, ptr, 6)) {
		return NT_STATUS_BUFFER_TOO_SMALL;
	}
	ofs  = SVAL(ptr, 0);
	size = IVAL(ptr, 2);
	if (ofs == 0) {
		*blob = data_blob(NULL, 0);
		return NT_STATUS_OK;
	}
	if (smb2_oob(buf, buf->hdr + ofs, size)) {
		return NT_STATUS_BUFFER_TOO_SMALL;
	}
	*blob = data_blob_talloc(mem_ctx, buf->hdr + ofs, size);
	NT_STATUS_HAVE_NO_MEMORY(blob->data);
	return NT_STATUS_OK;
}

/*
  pull a uint32_t ofs/ uint32_t length/blob triple from a data blob
  the ptr points to the start of the offset/length pair
*/
NTSTATUS smb2_pull_o32s32_blob(struct smb2_request_buffer *buf, TALLOC_CTX *mem_ctx, uint8_t *ptr, DATA_BLOB *blob)
{
	uint32_t ofs, size;
	if (smb2_oob(buf, ptr, 8)) {
		return NT_STATUS_BUFFER_TOO_SMALL;
	}
	ofs  = IVAL(ptr, 0);
	size = IVAL(ptr, 4);
	if (ofs == 0) {
		*blob = data_blob(NULL, 0);
		return NT_STATUS_OK;
	}
	if (smb2_oob(buf, buf->hdr + ofs, size)) {
		return NT_STATUS_BUFFER_TOO_SMALL;
	}
	*blob = data_blob_talloc(mem_ctx, buf->hdr + ofs, size);
	NT_STATUS_HAVE_NO_MEMORY(blob->data);
	return NT_STATUS_OK;
}

/*
  pull a uint16_t ofs/ uint32_t length/blob triple from a data blob
  the ptr points to the start of the offset/length pair
  
  In this varient the uint16_t is padded by an extra 2 bytes, making
  the size aligned on 4 byte boundary
*/
NTSTATUS smb2_pull_o16As32_blob(struct smb2_request_buffer *buf, TALLOC_CTX *mem_ctx, uint8_t *ptr, DATA_BLOB *blob)
{
	uint32_t ofs, size;
	if (smb2_oob(buf, ptr, 8)) {
		return NT_STATUS_BUFFER_TOO_SMALL;
	}
	ofs  = SVAL(ptr, 0);
	size = IVAL(ptr, 4);
	if (ofs == 0) {
		*blob = data_blob(NULL, 0);
		return NT_STATUS_OK;
	}
	if (smb2_oob(buf, buf->hdr + ofs, size)) {
		return NT_STATUS_BUFFER_TOO_SMALL;
	}
	*blob = data_blob_talloc(mem_ctx, buf->hdr + ofs, size);
	NT_STATUS_HAVE_NO_MEMORY(blob->data);
	return NT_STATUS_OK;
}

/*
  pull a uint32_t length/ uint32_t ofs/blob triple from a data blob
  the ptr points to the start of the offset/length pair
*/
NTSTATUS smb2_pull_s32o32_blob(struct smb2_request_buffer *buf, TALLOC_CTX *mem_ctx, uint8_t *ptr, DATA_BLOB *blob)
{
	uint32_t ofs, size;
	if (smb2_oob(buf, ptr, 8)) {
		return NT_STATUS_BUFFER_TOO_SMALL;
	}
	size = IVAL(ptr, 0);
	ofs  = IVAL(ptr, 4);
	if (ofs == 0) {
		*blob = data_blob(NULL, 0);
		return NT_STATUS_OK;
	}
	if (smb2_oob(buf, buf->hdr + ofs, size)) {
		return NT_STATUS_BUFFER_TOO_SMALL;
	}
	*blob = data_blob_talloc(mem_ctx, buf->hdr + ofs, size);
	NT_STATUS_HAVE_NO_MEMORY(blob->data);
	return NT_STATUS_OK;
}

/*
  pull a string in a uint16_t ofs/ uint16_t length/blob format
  UTF-16 without termination
*/
NTSTATUS smb2_pull_o16s16_string(struct smb2_request_buffer *buf, TALLOC_CTX *mem_ctx,
				 uint8_t *ptr, const char **str)
{
	DATA_BLOB blob;
	NTSTATUS status;
	ssize_t size;
	void *vstr;

	status = smb2_pull_o16s16_blob(buf, mem_ctx, ptr, &blob);
	NT_STATUS_NOT_OK_RETURN(status);

	if (blob.data == NULL) {
		*str = NULL;
		return NT_STATUS_OK;
	}

	if (blob.length == 0) {
		char *s;
		s = talloc_strdup(mem_ctx, "");
		NT_STATUS_HAVE_NO_MEMORY(s);
		*str = s;
		return NT_STATUS_OK;
	}

	size = convert_string_talloc(mem_ctx, lp_iconv_convenience(global_loadparm), CH_UTF16, CH_UNIX, 
				     blob.data, blob.length, &vstr);
	data_blob_free(&blob);
	(*str) = (char *)vstr;
	if (size == -1) {
		return NT_STATUS_ILLEGAL_CHARACTER;
	}
	return NT_STATUS_OK;
}

/*
  push a string in a uint16_t ofs/ uint16_t length/blob format
  UTF-16 without termination
*/
NTSTATUS smb2_push_o16s16_string(struct smb2_request_buffer *buf,
				 uint16_t ofs, const char *str)
{
	DATA_BLOB blob;
	NTSTATUS status;
	ssize_t size;

	if (str == NULL) {
		return smb2_push_o16s16_blob(buf, ofs, data_blob(NULL, 0));
	}

	if (*str == 0) {
		blob.data = discard_const(str);
		blob.length = 0;
		return smb2_push_o16s16_blob(buf, ofs, blob);
	}

	size = convert_string_talloc(buf->buffer, lp_iconv_convenience(global_loadparm), CH_UNIX, CH_UTF16, 
				     str, strlen(str), (void **)&blob.data);
	if (size == -1) {
		return NT_STATUS_ILLEGAL_CHARACTER;
	}
	blob.length = size;

	status = smb2_push_o16s16_blob(buf, ofs, blob);
	data_blob_free(&blob);
	return status;
}

/*
  push a file handle into a buffer
*/
void smb2_push_handle(uint8_t *data, struct smb2_handle *h)
{
	SBVAL(data, 0, h->data[0]);
	SBVAL(data, 8, h->data[1]);
}

/*
  pull a file handle from a buffer
*/
void smb2_pull_handle(uint8_t *ptr, struct smb2_handle *h)
{
	h->data[0] = BVAL(ptr, 0);
	h->data[1] = BVAL(ptr, 8);
}
