/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * Copyright (c) 2007, 2010, Oracle and/or its affiliates. All rights reserved.
 * Copyright 2020 Tintri by DDN, Inc.  All rights reserved.
 * Copyright 2022 RackTop Systems, Inc.
 */

/*
 * smb1 oplock support
 */

#include <smbsrv/smb_kproto.h>

#define	BATCH_OR_EXCL	(OPLOCK_LEVEL_BATCH | OPLOCK_LEVEL_ONE)

/*
 * This is called by the SMB1 "Locking_andX" handler,
 * for SMB1 oplock break acknowledgement.
 * This is an "Ack" from the client.
 */
void
smb1_oplock_ack_break(smb_request_t *sr, uchar_t oplock_level)
{
	smb_ofile_t	*ofile;
	smb_node_t	*node;
	uint32_t	NewLevel;

	ofile = sr->fid_ofile;
	node = ofile->f_node;

	if (oplock_level == 0)
		NewLevel = OPLOCK_LEVEL_NONE;
	else
		NewLevel = OPLOCK_LEVEL_TWO;

	smb_llist_enter(&node->n_ofile_list, RW_READER);
	mutex_enter(&node->n_oplock.ol_mutex);

	ofile->f_oplock.og_breaking = B_FALSE;
	cv_broadcast(&ofile->f_oplock.og_ack_cv);

	(void) smb_oplock_ack_break(sr, ofile, &NewLevel);

	ofile->f_oplock.og_state = NewLevel;

	mutex_exit(&node->n_oplock.ol_mutex);
	smb_llist_exit(&node->n_ofile_list);
}

/*
 * Compose an SMB1 Oplock Break Notification packet, including
 * the SMB1 header and everything, in sr->reply.
 * The caller will send it and free the request.
 */
static void
smb1_oplock_break_notification(smb_request_t *sr, uint32_t NewLevel)
{
	smb_ofile_t *ofile = sr->fid_ofile;
	uint16_t fid;
	uint8_t lock_type;
	uint8_t oplock_level;

	/*
	 * Convert internal level to SMB1
	 */
	switch (NewLevel) {
	default:
		ASSERT(0);
		/* FALLTHROUGH */
	case OPLOCK_LEVEL_NONE:
		oplock_level = 0;
		break;

	case OPLOCK_LEVEL_TWO:
		oplock_level = 1;
		break;
	}

	sr->smb_com = SMB_COM_LOCKING_ANDX;
	sr->smb_tid = ofile->f_tree->t_tid;
	sr->smb_pid = 0xFFFF;
	sr->smb_uid = 0;
	sr->smb_mid = 0xFFFF;
	fid = ofile->f_fid;
	lock_type = LOCKING_ANDX_OPLOCK_RELEASE;

	(void) smb_mbc_encodef(
	    &sr->reply, "Mb19.wwwwbb3.wbb10.",
	    /*  "\xffSMB"		   M */
	    sr->smb_com,		/* b */
	    /* status, flags, signature	 19. */
	    sr->smb_tid,		/* w */
	    sr->smb_pid,		/* w */
	    sr->smb_uid,		/* w */
	    sr->smb_mid,		/* w */
	    8,		/* word count	   b */
	    0xFF,	/* AndX cmd	   b */
	    /*  AndX reserved, offset	  3. */
	    fid,
	    lock_type,
	    oplock_level);
}

/*
 * Send an oplock break over the wire, or if we can't,
 * then process the oplock break locally.
 *
 * [MS-CIFS] 3.3.4.2 Object Store Indicates an OpLock Break
 *
 * This is mostly similar to smb2_oplock_send_break()
 * See top comment there about the design.
 * Called from smb_oplock_async_break.
 *
 * This handles only SMB1, which has no durable handles,
 * and never has GRANULAR oplocks.
 */
void
smb1_oplock_send_break(smb_request_t *sr)
{
	smb_ofile_t	*ofile = sr->fid_ofile;
	smb_node_t	*node = ofile->f_node;
	uint32_t	NewLevel = sr->arg.olbrk.NewLevel;
	boolean_t	AckReq = sr->arg.olbrk.AckRequired;
	uint32_t	status;
	int		rc;

	/*
	 * SMB1 clients should only get Level II oplocks if they
	 * set the capability indicating they know about them.
	 */
	if (NewLevel == OPLOCK_LEVEL_TWO &&
	    ofile->f_oplock.og_dialect < NT_LM_0_12)
		NewLevel = OPLOCK_LEVEL_NONE;

	/*
	 * Build the break message in sr->reply.
	 * It's free'd in smb_request_free().
	 * Always SMB1 here.
	 */
	sr->reply.max_bytes = MLEN;
	smb1_oplock_break_notification(sr, NewLevel);

	/*
	 * Try to send the break message to the client.
	 * If connected, this IF body will be true.
	 */
	if (sr->session == ofile->f_session)
		rc = smb_session_send(sr->session, 0, &sr->reply);
	else
		rc = ENOTCONN;

	if (rc != 0) {
		/*
		 * We were unable to send the oplock break request,
		 * presumably because the connection is gone.
		 * Just close the handle.
		 */
		smb_ofile_close(ofile, 0);
		return;
	}

	/*
	 * OK, we were able to send the break message.
	 * If no ack. required, we're done.
	 */
	if (!AckReq)
		return;

	/*
	 * We're expecting an ACK.  Wait in this thread
	 * so we can log clients that don't respond.
	 */
	status = smb_oplock_wait_ack(sr, NewLevel);
	if (status == 0)
		return;

	cmn_err(CE_NOTE, "clnt %s oplock break timeout",
	    sr->session->ip_addr_str);
	DTRACE_PROBE1(ack_timeout, smb_request_t *, sr);

	/*
	 * Did not get an ACK, so do the ACK locally.
	 * Note: always break to none here, regardless
	 * of what the passed in cache level was.
	 */
	NewLevel = OPLOCK_LEVEL_NONE;

	smb_llist_enter(&node->n_ofile_list, RW_READER);
	mutex_enter(&node->n_oplock.ol_mutex);

	ofile->f_oplock.og_breaking = B_FALSE;
	cv_broadcast(&ofile->f_oplock.og_ack_cv);

	status = smb_oplock_ack_break(sr, ofile, &NewLevel);

	ofile->f_oplock.og_state = NewLevel;

	mutex_exit(&node->n_oplock.ol_mutex);
	smb_llist_exit(&node->n_ofile_list);

#ifdef	DEBUG
	if (status != 0) {
		cmn_err(CE_NOTE, "clnt %s local oplock ack, status=0x%x",
		    sr->session->ip_addr_str, status);
	}
#endif
}

/*
 * Client has an open handle and requests an oplock.
 * Convert SMB1 oplock request info in to internal form,
 * call common oplock code, convert result to SMB1.
 */
void
smb1_oplock_acquire(smb_request_t *sr, boolean_t level2ok)
{
	smb_arg_open_t *op = &sr->arg.open;
	smb_ofile_t *ofile = sr->fid_ofile;
	uint32_t status;

	/* Only disk trees get oplocks. */
	if ((sr->tid_tree->t_res_type & STYPE_MASK) != STYPE_DISKTREE) {
		op->op_oplock_level = SMB_OPLOCK_NONE;
		return;
	}

	if (!smb_tree_has_feature(sr->tid_tree, SMB_TREE_OPLOCKS)) {
		op->op_oplock_level = SMB_OPLOCK_NONE;
		return;
	}

	if (!smb_session_levelII_oplocks(sr->session))
		level2ok = B_FALSE;

	/* Common code checks file type. */

	/*
	 * SMB1: Convert to internal form.
	 */
	switch (op->op_oplock_level) {
	case SMB_OPLOCK_BATCH:
		op->op_oplock_state = OPLOCK_LEVEL_BATCH;
		break;
	case SMB_OPLOCK_EXCLUSIVE:
		op->op_oplock_state = OPLOCK_LEVEL_ONE;
		break;
	case SMB_OPLOCK_LEVEL_II:
		op->op_oplock_state = OPLOCK_LEVEL_TWO;
		break;
	case SMB_OPLOCK_NONE:
	default:
		op->op_oplock_level = SMB_OPLOCK_NONE;
		return;
	}

	/*
	 * Tree options may force shared oplocks
	 */
	if (smb_tree_has_feature(sr->tid_tree, SMB_TREE_FORCE_L2_OPLOCK)) {
		op->op_oplock_state = OPLOCK_LEVEL_TWO;
	}

	/*
	 * Try exclusive first, if requested
	 */
	if ((op->op_oplock_state & BATCH_OR_EXCL) != 0) {
		status = smb_oplock_request(sr, ofile,
		    &op->op_oplock_state);
	} else {
		status = NT_STATUS_OPLOCK_NOT_GRANTED;
	}

	/*
	 * If exclusive failed (or the tree forced shared oplocks)
	 * and if the caller supports Level II, try shared.
	 */
	if (status == NT_STATUS_OPLOCK_NOT_GRANTED && level2ok) {
		op->op_oplock_state = OPLOCK_LEVEL_TWO;
		status = smb_oplock_request(sr, ofile,
		    &op->op_oplock_state);
	}

	/*
	 * Keep track of what we got (ofile->f_oplock.og_state etc)
	 * so we'll know what we had when sending a break later.
	 * The og_dialect here is the oplock dialect, which may be
	 * different than SMB dialect.  Pre-NT clients did not
	 * support "Level II" oplocks.  If we're talking to a
	 * client that didn't set the CAP_LEVEL_II_OPLOCKS in
	 * its capabilities, let og_dialect = LANMAN2_1.
	 */
	switch (status) {
	case NT_STATUS_SUCCESS:
	case NT_STATUS_OPLOCK_BREAK_IN_PROGRESS:
		ofile->f_oplock.og_dialect = (level2ok) ?
		    NT_LM_0_12 : LANMAN2_1;
		ofile->f_oplock.og_state   = op->op_oplock_state;
		ofile->f_oplock.og_breakto = op->op_oplock_state;
		ofile->f_oplock.og_breaking = B_FALSE;
		break;
	case NT_STATUS_OPLOCK_NOT_GRANTED:
		op->op_oplock_level = SMB_OPLOCK_NONE;
		return;
	default:
		/* Caller did not check args sufficiently? */
		cmn_err(CE_NOTE, "clnt %s oplock req. err 0x%x",
		    sr->session->ip_addr_str, status);
		op->op_oplock_level = SMB_OPLOCK_NONE;
		return;
	}

	/*
	 * Only succes cases get here.
	 * Convert internal oplock state to SMB1
	 */
	if (op->op_oplock_state & OPLOCK_LEVEL_BATCH) {
		op->op_oplock_level = SMB_OPLOCK_BATCH;
	} else if (op->op_oplock_state & OPLOCK_LEVEL_ONE) {
		op->op_oplock_level = SMB_OPLOCK_EXCLUSIVE;
	} else if (op->op_oplock_state & OPLOCK_LEVEL_TWO) {
		op->op_oplock_level = SMB_OPLOCK_LEVEL_II;
	} else {
		op->op_oplock_level = SMB_OPLOCK_NONE;
	}

	/*
	 * An smb_oplock_reqest call may have returned the
	 * status code that says we should wait.
	 */
	if (status == NT_STATUS_OPLOCK_BREAK_IN_PROGRESS) {
		(void) smb_oplock_wait_break(sr, ofile->f_node, 0);
	}
}