/*
 * Generic advertisement service (GAS) server
 * Copyright (c) 2017, Qualcomm Atheros, Inc.
 * Copyright (c) 2020, The Linux Foundation
 *
 * This software may be distributed under the terms of the BSD license.
 * See README for more details.
 */

#include "includes.h"

#include "utils/common.h"
#include "utils/list.h"
#include "utils/eloop.h"
#include "ieee802_11_defs.h"
#include "gas.h"
#include "gas_server.h"


#define MAX_ADV_PROTO_ID_LEN 10
#define GAS_QUERY_TIMEOUT 10

struct gas_server_handler {
	struct dl_list list;
	u8 adv_proto_id[MAX_ADV_PROTO_ID_LEN];
	u8 adv_proto_id_len;
	struct wpabuf * (*req_cb)(void *ctx, void *resp_ctx, const u8 *sa,
				  const u8 *query, size_t query_len,
				  u16 *comeback_delay);
	void (*status_cb)(void *ctx, struct wpabuf *resp, int ok);
	void *ctx;
	struct gas_server *gas;
};

struct gas_server_response {
	struct dl_list list;
	size_t offset;
	u8 frag_id;
	struct wpabuf *resp;
	int freq;
	u8 dst[ETH_ALEN];
	u8 dialog_token;
	struct gas_server_handler *handler;
	u16 comeback_delay;
};

struct gas_server {
	struct dl_list handlers; /* struct gas_server_handler::list */
	struct dl_list responses; /* struct gas_server_response::list */
	void (*tx)(void *ctx, int freq, const u8 *da, struct wpabuf *resp,
		   unsigned int wait_time);
	void *ctx;
};

static void gas_server_free_response(struct gas_server_response *response);


static void gas_server_response_timeout(void *eloop_ctx, void *user_ctx)
{
	struct gas_server_response *response = eloop_ctx;

	wpa_printf(MSG_DEBUG, "GAS: Response @%p timeout for " MACSTR
		   " (dialog_token=%u freq=%d frag_id=%u sent=%lu/%lu) - drop pending data",
		   response, MAC2STR(response->dst), response->dialog_token,
		   response->freq, response->frag_id,
		   (unsigned long) response->offset,
		   (unsigned long) (response->resp ?
				    wpabuf_len(response->resp) : 0));
	response->handler->status_cb(response->handler->ctx,
				     response->resp, 0);
	response->resp = NULL;
	dl_list_del(&response->list);
	gas_server_free_response(response);
}


static void gas_server_free_response(struct gas_server_response *response)
{
	if (!response)
		return;
	wpa_printf(MSG_DEBUG, "DPP: Free GAS response @%p", response);
	eloop_cancel_timeout(gas_server_response_timeout, response, NULL);
	wpabuf_free(response->resp);
	os_free(response);
}


static void
gas_server_send_resp(struct gas_server *gas, struct gas_server_handler *handler,
		     struct gas_server_response *response,
		     const u8 *da, int freq, u8 dialog_token,
		     struct wpabuf *query_resp, u16 comeback_delay)
{
	size_t max_len = (freq > 56160) ? 928 : 1400;
	size_t hdr_len = 24 + 2 + 5 + 3 + handler->adv_proto_id_len + 2;
	size_t resp_frag_len;
	struct wpabuf *resp;

	if (comeback_delay == 0 && !query_resp) {
		gas_server_free_response(response);
		return;
	}

	response->freq = freq;
	response->handler = handler;
	os_memcpy(response->dst, da, ETH_ALEN);
	response->dialog_token = dialog_token;
	if (comeback_delay) {
		/* Need more time to prepare the response */
		resp_frag_len = 0;
		response->comeback_delay = comeback_delay;
	} else if (hdr_len + wpabuf_len(query_resp) > max_len) {
		/* Need to use comeback to initiate fragmentation */
		comeback_delay = 1;
		resp_frag_len = 0;
	} else {
		/* Full response fits into the initial response */
		comeback_delay = 0;
		resp_frag_len = wpabuf_len(query_resp);
	}

	resp = gas_build_initial_resp(dialog_token, WLAN_STATUS_SUCCESS,
				      comeback_delay,
				      handler->adv_proto_id_len +
				      resp_frag_len);
	if (!resp) {
		wpabuf_free(query_resp);
		gas_server_free_response(response);
		return;
	}

	/* Advertisement Protocol element */
	wpabuf_put_u8(resp, WLAN_EID_ADV_PROTO);
	wpabuf_put_u8(resp, 1 + handler->adv_proto_id_len); /* Length */
	wpabuf_put_u8(resp, 0x7f);
	/* Advertisement Protocol ID */
	wpabuf_put_data(resp, handler->adv_proto_id, handler->adv_proto_id_len);

	/* Query Response Length */
	wpabuf_put_le16(resp, resp_frag_len);
	if (!comeback_delay && query_resp)
		wpabuf_put_buf(resp, query_resp);

	if (comeback_delay && !query_resp) {
		wpa_printf(MSG_DEBUG, "GAS: No response available yet");
	} else if (comeback_delay) {
		wpa_printf(MSG_DEBUG,
			   "GAS: Need to fragment query response");
	} else {
		wpa_printf(MSG_DEBUG,
			   "GAS: Full query response fits in the GAS Initial Response frame");
	}
	response->offset = resp_frag_len;
	response->resp = query_resp;
	dl_list_add(&gas->responses, &response->list);
	gas->tx(gas->ctx, freq, da, resp, comeback_delay ? 2000 : 0);
	wpabuf_free(resp);
	eloop_register_timeout(GAS_QUERY_TIMEOUT, 0,
			       gas_server_response_timeout, response, NULL);
}


static int
gas_server_rx_initial_req(struct gas_server *gas, const u8 *da, const u8 *sa,
			  const u8 *bssid, int freq, u8 dialog_token,
			  const u8 *data, size_t len)
{
	const u8 *pos, *end, *adv_proto, *query_req;
	u8 adv_proto_len;
	u16 query_req_len;
	struct gas_server_handler *handler;
	struct wpabuf *resp;
	struct gas_server_response *response;

	wpa_hexdump(MSG_MSGDUMP, "GAS: Received GAS Initial Request frame",
		    data, len);
	pos = data;
	end = data + len;

	if (end - pos < 2 || pos[0] != WLAN_EID_ADV_PROTO) {
		wpa_printf(MSG_DEBUG,
			   "GAS: No Advertisement Protocol element found");
		return -1;
	}
	pos++;
	adv_proto_len = *pos++;
	if (end - pos < adv_proto_len || adv_proto_len < 2) {
		wpa_printf(MSG_DEBUG,
			   "GAS: Truncated Advertisement Protocol element");
		return -1;
	}

	adv_proto = pos;
	pos += adv_proto_len;
	wpa_hexdump(MSG_MSGDUMP, "GAS: Advertisement Protocol element",
		    adv_proto, adv_proto_len);

	if (end - pos < 2) {
		wpa_printf(MSG_DEBUG, "GAS: No Query Request Length field");
		return -1;
	}
	query_req_len = WPA_GET_LE16(pos);
	pos += 2;
	if (end - pos < query_req_len) {
		wpa_printf(MSG_DEBUG, "GAS: Truncated Query Request field");
		return -1;
	}
	query_req = pos;
	pos += query_req_len;
	wpa_hexdump(MSG_MSGDUMP, "GAS: Query Request",
		    query_req, query_req_len);

	if (pos < end) {
		wpa_hexdump(MSG_MSGDUMP,
			    "GAS: Ignored extra data after Query Request field",
			    pos, end - pos);
	}

	response = os_zalloc(sizeof(*response));
	if (!response)
		return -1;

	wpa_printf(MSG_DEBUG, "DPP: Allocated GAS response @%p", response);
	dl_list_for_each(handler, &gas->handlers, struct gas_server_handler,
			 list) {
		u16 comeback_delay = 0;

		if (adv_proto_len < 1 + handler->adv_proto_id_len ||
		    os_memcmp(adv_proto + 1, handler->adv_proto_id,
			      handler->adv_proto_id_len) != 0)
			continue;

		wpa_printf(MSG_DEBUG,
			   "GAS: Calling handler for the requested Advertisement Protocol ID");
		resp = handler->req_cb(handler->ctx, response, sa, query_req,
				       query_req_len, &comeback_delay);
		wpa_hexdump_buf(MSG_MSGDUMP, "GAS: Response from the handler",
				resp);
		if (comeback_delay)
			wpa_printf(MSG_DEBUG,
				   "GAS: Handler requested comeback delay: %u TU",
				   comeback_delay);
		gas_server_send_resp(gas, handler, response, sa, freq,
				     dialog_token, resp, comeback_delay);
		return 0;
	}

	wpa_printf(MSG_DEBUG,
		   "GAS: No registered handler for the requested Advertisement Protocol ID");
	gas_server_free_response(response);
	return -1;
}


static void
gas_server_handle_rx_comeback_req(struct gas_server_response *response)
{
	struct gas_server_handler *handler = response->handler;
	struct gas_server *gas = handler->gas;
	size_t max_len = (response->freq > 56160) ? 928 : 1400;
	size_t hdr_len = 24 + 2 + 6 + 3 + handler->adv_proto_id_len + 2;
	size_t remaining, resp_frag_len;
	struct wpabuf *resp;
	unsigned int wait_time = 0;

	if (!response->resp) {
		resp = gas_build_comeback_resp(response->dialog_token,
					       WLAN_STATUS_SUCCESS, 0, 0,
					       response->comeback_delay,
					       handler->adv_proto_id_len);
		if (!resp) {
			dl_list_del(&response->list);
			gas_server_free_response(response);
			return;
		}

		/* Advertisement Protocol element */
		wpabuf_put_u8(resp, WLAN_EID_ADV_PROTO);
		wpabuf_put_u8(resp, 1 + handler->adv_proto_id_len); /* Length */
		wpabuf_put_u8(resp, 0x7f);
		/* Advertisement Protocol ID */
		wpabuf_put_data(resp, handler->adv_proto_id,
				handler->adv_proto_id_len);

		/* Query Response Length */
		wpabuf_put_le16(resp, 0);
		goto send_resp;
	}

	remaining = wpabuf_len(response->resp) - response->offset;
	if (hdr_len + remaining > max_len)
		resp_frag_len = max_len - hdr_len;
	else
		resp_frag_len = remaining;
	wpa_printf(MSG_DEBUG,
		   "GAS: Sending out %u/%u remaining Query Response octets",
		   (unsigned int) resp_frag_len, (unsigned int) remaining);

	resp = gas_build_comeback_resp(response->dialog_token,
				       WLAN_STATUS_SUCCESS,
				       response->frag_id++,
				       resp_frag_len < remaining, 0,
				       handler->adv_proto_id_len +
				       resp_frag_len);
	if (!resp) {
		dl_list_del(&response->list);
		gas_server_free_response(response);
		return;
	}

	/* Advertisement Protocol element */
	wpabuf_put_u8(resp, WLAN_EID_ADV_PROTO);
	wpabuf_put_u8(resp, 1 + handler->adv_proto_id_len); /* Length */
	wpabuf_put_u8(resp, 0x7f);
	/* Advertisement Protocol ID */
	wpabuf_put_data(resp, handler->adv_proto_id, handler->adv_proto_id_len);

	/* Query Response Length */
	wpabuf_put_le16(resp, resp_frag_len);
	wpabuf_put_data(resp, wpabuf_head_u8(response->resp) + response->offset,
			resp_frag_len);

	response->offset += resp_frag_len;

	if (remaining > resp_frag_len)
		wait_time = 2000;

send_resp:
	gas->tx(gas->ctx, response->freq, response->dst, resp, wait_time);
	wpabuf_free(resp);
}


static int
gas_server_rx_comeback_req(struct gas_server *gas, const u8 *da, const u8 *sa,
			   const u8 *bssid, int freq, u8 dialog_token)
{
	struct gas_server_response *response;

	dl_list_for_each(response, &gas->responses, struct gas_server_response,
			 list) {
		if (response->dialog_token != dialog_token ||
		    os_memcmp(sa, response->dst, ETH_ALEN) != 0)
			continue;
		gas_server_handle_rx_comeback_req(response);
		return 0;
	}

	wpa_printf(MSG_DEBUG, "GAS: No pending GAS response for " MACSTR
		   " (dialog token %u)", MAC2STR(sa), dialog_token);
	return -1;
}


/**
 * gas_query_rx - Indicate reception of a Public Action or Protected Dual frame
 * @gas: GAS query data from gas_server_init()
 * @da: Destination MAC address of the Action frame
 * @sa: Source MAC address of the Action frame
 * @bssid: BSSID of the Action frame
 * @categ: Category of the Action frame
 * @data: Payload of the Action frame
 * @len: Length of @data
 * @freq: Frequency (in MHz) on which the frame was received
 * Returns: 0 if the Public Action frame was a GAS request frame or -1 if not
 */
int gas_server_rx(struct gas_server *gas, const u8 *da, const u8 *sa,
		  const u8 *bssid, u8 categ, const u8 *data, size_t len,
		  int freq)
{
	u8 action, dialog_token;
	const u8 *pos, *end;

	if (!gas || len < 2)
		return -1;

	if (categ == WLAN_ACTION_PROTECTED_DUAL)
		return -1; /* Not supported for now */

	pos = data;
	end = data + len;
	action = *pos++;
	dialog_token = *pos++;

	if (action != WLAN_PA_GAS_INITIAL_REQ &&
	    action != WLAN_PA_GAS_COMEBACK_REQ)
		return -1; /* Not a GAS request */

	wpa_printf(MSG_DEBUG, "GAS: Received GAS %s Request frame DA=" MACSTR
		   " SA=" MACSTR " BSSID=" MACSTR
		   " freq=%d dialog_token=%u len=%u",
		   action == WLAN_PA_GAS_INITIAL_REQ ? "Initial" : "Comeback",
		   MAC2STR(da), MAC2STR(sa), MAC2STR(bssid), freq, dialog_token,
		   (unsigned int) len);

	if (action == WLAN_PA_GAS_INITIAL_REQ)
		return gas_server_rx_initial_req(gas, da, sa, bssid,
						 freq, dialog_token,
						 pos, end - pos);
	return gas_server_rx_comeback_req(gas, da, sa, bssid,
					  freq, dialog_token);
}


static void gas_server_handle_tx_status(struct gas_server_response *response,
					int ack)
{
	if (ack && response->resp &&
	    response->offset < wpabuf_len(response->resp)) {
		wpa_printf(MSG_DEBUG,
			   "GAS: More fragments remaining - keep pending entry");
		return;
	}

	if (ack && !response->resp && response->comeback_delay) {
		wpa_printf(MSG_DEBUG,
			   "GAS: Waiting for response - keep pending entry");
		return;
	}

	if (!ack)
		wpa_printf(MSG_DEBUG,
			   "GAS: No ACK received - drop pending entry");
	else
		wpa_printf(MSG_DEBUG,
			   "GAS: Last fragment of the response sent out - drop pending entry");

	response->handler->status_cb(response->handler->ctx,
				     response->resp, ack);
	response->resp = NULL;
	dl_list_del(&response->list);
	gas_server_free_response(response);
}


void gas_server_tx_status(struct gas_server *gas, const u8 *dst, const u8 *data,
			  size_t data_len, int ack)
{
	const u8 *pos;
	u8 action, code, dialog_token;
	struct gas_server_response *response;

	if (data_len < 24 + 3)
		return;
	pos = data + 24;
	action = *pos++;
	code = *pos++;
	dialog_token = *pos++;
	if (action != WLAN_ACTION_PUBLIC ||
	    (code != WLAN_PA_GAS_INITIAL_RESP &&
	     code != WLAN_PA_GAS_COMEBACK_RESP))
		return;
	wpa_printf(MSG_DEBUG, "GAS: TX status dst=" MACSTR
		   " ack=%d %s dialog_token=%u",
		   MAC2STR(dst), ack,
		   code == WLAN_PA_GAS_INITIAL_RESP ? "initial" : "comeback",
		   dialog_token);
	dl_list_for_each(response, &gas->responses, struct gas_server_response,
			 list) {
		if (response->dialog_token != dialog_token ||
		    os_memcmp(dst, response->dst, ETH_ALEN) != 0)
			continue;
		gas_server_handle_tx_status(response, ack);
		return;
	}

	wpa_printf(MSG_DEBUG, "GAS: No pending response matches TX status");
}


int gas_server_set_resp(struct gas_server *gas, void *resp_ctx,
			struct wpabuf *resp)
{
	struct gas_server_response *tmp, *response = NULL;

	dl_list_for_each(tmp, &gas->responses, struct gas_server_response,
			 list) {
		if (tmp == resp_ctx) {
			response = tmp;
			break;
		}
	}

	if (!response || response->resp)
		return -1;

	response->resp = resp;
	return 0;
}


bool gas_server_response_sent(struct gas_server *gas, void *resp_ctx)
{
	struct gas_server_response *tmp;

	dl_list_for_each(tmp, &gas->responses, struct gas_server_response,
			 list) {
		if (tmp == resp_ctx)
			return tmp->resp &&
				tmp->offset == wpabuf_len(tmp->resp);
	}

	return false;
}


struct gas_server * gas_server_init(void *ctx,
				    void (*tx)(void *ctx, int freq,
					       const u8 *da,
					       struct wpabuf *buf,
					       unsigned int wait_time))
{
	struct gas_server *gas;

	gas = os_zalloc(sizeof(*gas));
	if (!gas)
		return NULL;
	gas->ctx = ctx;
	gas->tx = tx;
	dl_list_init(&gas->handlers);
	dl_list_init(&gas->responses);
	return gas;
}


void gas_server_deinit(struct gas_server *gas)
{
	struct gas_server_handler *handler, *tmp;
	struct gas_server_response *response, *tmp_r;

	if (!gas)
		return;

	dl_list_for_each_safe(handler, tmp, &gas->handlers,
			      struct gas_server_handler, list) {
		dl_list_del(&handler->list);
		os_free(handler);
	}

	dl_list_for_each_safe(response, tmp_r, &gas->responses,
			      struct gas_server_response, list) {
		dl_list_del(&response->list);
		gas_server_free_response(response);
	}

	os_free(gas);
}


int gas_server_register(struct gas_server *gas,
			const u8 *adv_proto_id, u8 adv_proto_id_len,
			struct wpabuf *
			(*req_cb)(void *ctx, void *resp_ctx, const u8 *sa,
				  const u8 *query, size_t query_len,
				  u16 *comeback_delay),
			void (*status_cb)(void *ctx, struct wpabuf *resp,
					  int ok),
			void *ctx)
{
	struct gas_server_handler *handler;

	if (!gas || adv_proto_id_len > MAX_ADV_PROTO_ID_LEN)
		return -1;
	handler = os_zalloc(sizeof(*handler));
	if (!handler)
		return -1;

	os_memcpy(handler->adv_proto_id, adv_proto_id, adv_proto_id_len);
	handler->adv_proto_id_len = adv_proto_id_len;
	handler->req_cb = req_cb;
	handler->status_cb = status_cb;
	handler->ctx = ctx;
	handler->gas = gas;
	dl_list_add(&gas->handlers, &handler->list);

	return 0;
}
