#include "logger.h"
#include <tfe_scan.h>

extern void increase_redirect_policy_hit_num(void);

#define DOH_CTX_MAGIC_NUM 20200601

#define REQ_METHOD_IS_GET(method) ((method == TFE_HTTP_METHOD_GET) ? 1 : 0)
#define REQ_METHOD_IS_POST(method) ((method == TFE_HTTP_METHOD_POST) ? 1 : 0)

struct dns_str2idx
{
	int index;
	int len;
	char *type;
};

struct dns_str2idx str2index[] = {
	{DNS_TYPE_CNAME, 5, (char *)"CNAME"},
	{DNS_TYPE_MX, 2, (char *)"MX"},
	{DNS_TYPE_A, 1, (char *)"A"},
	{DNS_TYPE_NS, 2, (char *)"NS"},
	{DNS_TYPE_AAAA, 4, (char *)"AAAA"},
	{DNS_TYPE_TXT, 3, (char *)"TXT"},
	{DNS_TYPE_PTR, 3, (char *)"PTR"}};

static struct doh_conf *g_doh_conf = NULL;

static int doh_type2index(char *type)
{
	int i = 0;
	for (i = 0; i < (int)(sizeof(str2index) / sizeof(struct dns_str2idx)); i++)
	{
		if (str2index[i].len == (int)strlen(type) && (strncasecmp(str2index[i].type, type, str2index[i].len)) == 0)
		{
			return str2index[i].index;
		}
	}

	return -1;
}

static void doh_addr_tfe2sapp(const struct tfe_stream_addr *tfe_addr, struct ipaddr *sapp_addr)
{
	if (tfe_addr->addrtype == TFE_ADDR_STREAM_TUPLE4_V4 || tfe_addr->addrtype == TFE_ADDR_IPV4)
	{
		sapp_addr->addrtype = ADDR_TYPE_IPV4;
	}
	else
	{
		sapp_addr->addrtype = ADDR_TYPE_IPV6;
	}
	sapp_addr->paddr = (char *)tfe_addr->paddr;
}

static int doh_get_answer_ttl(cJSON *object)
{
	int min = 0;
	int max = 0;
	cJSON *item = NULL;

	item = cJSON_GetObjectItem(object, "min");
	if (item)
	{
		min = item->valueint;
	}

	item = cJSON_GetObjectItem(object, "max");
	if (item)
	{
		max = item->valueint;
	}

	return (rand() % (max - min + 1) + min);
}

static cJSON *doh_get_answer_records(cJSON *object, int qtype)
{
	int i = 0;

	cJSON *resolution = cJSON_GetObjectItem(object, "resolution");
	int size = cJSON_GetArraySize(resolution);
	for (i = 0; i < size; i++)
	{
		cJSON *item = cJSON_GetArrayItem(resolution, i);
		cJSON *tmp = cJSON_GetObjectItem(item, "qtype");

		if (doh_type2index(tmp->valuestring) == qtype)
		{
			return cJSON_GetObjectItem(item, "answer");
		}
	}

	return NULL;
}

static void doh_get_cheat_data(Maat_rule_t *p_result, int qtype, struct doh_ctx *ctx, const char *str_stream_info)
{
	int i;
	int answer_size = 0;
	char *tmp = NULL;
	cJSON *items = NULL;
	cJSON *item = NULL;
	cJSON *object = NULL;
	cJSON *answer_array = NULL;

	tmp = (char *)calloc(1, p_result->serv_def_len + 1);
	Maat_read_rule(g_doh_conf->maat, p_result, MAAT_RULE_SERV_DEFINE, tmp, p_result->serv_def_len);
	TFE_LOG_INFO(g_doh_conf->local_logger, "%s hit %d %s", str_stream_info, p_result->config_id, tmp);

	object = cJSON_Parse(tmp);
	if (object == NULL)
	{
		goto end;
	}

	answer_array = doh_get_answer_records(object, qtype);
	if (answer_array != NULL)
	{
		answer_size = cJSON_GetArraySize(answer_array);
		ctx->opts = ALLOC(cheat_pkt_opt_t, answer_size);
		ctx->opts_num = answer_size;

		for (i = 0; i < answer_size; i++)
		{
			items = cJSON_GetArrayItem(answer_array, i);

			item = cJSON_GetObjectItem(items, "atype");
			ctx->opts[i].res_type = doh_type2index(item->valuestring);

			item = cJSON_GetObjectItem(items, "ttl");
			ctx->opts[i].ttl = doh_get_answer_ttl(item);

			if (ctx->min_ttl == 0)
			{
				ctx->min_ttl = ctx->opts[i].ttl;
			}
			else
			{
				if (ctx->min_ttl > ctx->opts[i].ttl)
				{
					ctx->min_ttl = ctx->opts[i].ttl;
				}
			}

			item = cJSON_GetObjectItem(items, "value");
			if (item)
			{
				switch (ctx->opts[i].res_type)
				{
				case DNS_TYPE_A:
					ctx->opts[i].res_len = sizeof(unsigned int);
					inet_pton(AF_INET, item->valuestring, (void *)ctx->opts[i].res_info);
					break;
				case DNS_TYPE_AAAA:
					ctx->opts[i].res_len = IPV6_ADDR_LEN;
					inet_pton(AF_INET6, item->valuestring, (void *)ctx->opts[i].res_info);
					break;
				default:
					ctx->opts[i].res_len = (strlen(item->valuestring) > sizeof(ctx->opts[i].res_info) - 1) ? sizeof(ctx->opts[i].res_info) - 1 : strlen(item->valuestring);
					memcpy(ctx->opts[i].res_info, item->valuestring, ctx->opts[i].res_len);
					break;
				}
			}
		}
	}

end:
	if (object)
	{
		cJSON_Delete(object);
		object = NULL;
	}

	if (tmp)
	{
		free(tmp);
		tmp = NULL;
	}
}

static struct Maat_rule_t *doh_fetch_rule(Maat_rule_t *result, int result_num)
{
	int i = 0;
	Maat_rule_t *p_result = NULL;

	for (i = 0; i < result_num && i < MAX_SCAN_RESULT; i++)
	{
		if (p_result == NULL)
		{
			p_result = &result[i];
			continue;
		}

		if (result[i].config_id > p_result->config_id)
		{
			p_result = &result[i];
		}
	}

	return p_result;
}

static void doh_maat_scan(const struct tfe_stream *stream, const struct tfe_http_session *session, struct doh_ctx *ctx, char *qname, int qtype)
{
	int hit_cnt = 0;
	int scan_ret = 0;
	int app_id = 8006;
	struct ipaddr sapp_addr;
	struct Maat_rule_t *p_result = NULL;
	struct Maat_rule_t result[MAX_SCAN_RESULT];

	scan_ret = tfe_scan_subscribe_id(stream, result, &(ctx->scan_mid), hit_cnt, ctx->thread_id, g_doh_conf->local_logger);
	if (scan_ret > 0)
	{
		hit_cnt += scan_ret;
	}

	scan_ret = tfe_scan_ip_location(stream, result, &(ctx->scan_mid), hit_cnt, ctx->thread_id, g_doh_conf->local_logger, &(ctx->location_server), &(ctx->location_client));
	if (scan_ret > 0)
	{
		hit_cnt += scan_ret;
	}

	scan_ret = tfe_scan_ip_asn(stream, result, &(ctx->scan_mid), hit_cnt, ctx->thread_id, g_doh_conf->local_logger, &(ctx->asn_server), &(ctx->asn_client));
	if (scan_ret > 0)
	{
		hit_cnt += scan_ret;
	}

	// scan server host
	const char *host = session->req->req_spec.host;
	if (host)
	{
		scan_ret = Maat_full_scan_string(g_doh_conf->maat, g_doh_conf->tables[TYPE_HOST].id, CHARSET_UTF8,
										 host, strlen(host), result + hit_cnt, NULL, MAX_SCAN_RESULT - hit_cnt, &(ctx->scan_mid), ctx->thread_id);
		if (scan_ret > 0)
		{
            TFE_LOG_INFO(g_doh_conf->local_logger, "Scan %s, Hit host: %s scan ret: %d policy_id: %d service: %d action: %d addr: %s",
                         g_doh_conf->tables[TYPE_HOST].name, host, scan_ret, result[hit_cnt].config_id, result[hit_cnt].service_id, result[hit_cnt].action, stream->str_stream_info);
            hit_cnt += scan_ret;
		}
		else
		{
            TFE_LOG_INFO(g_doh_conf->local_logger, "Scan %s, NO hit host: %s scan ret: %d addr: %s",
                         g_doh_conf->tables[TYPE_HOST].name, host, scan_ret, stream->str_stream_info);
        }

        scan_ret = tfe_scan_fqdn_cat(stream, result, &(ctx->scan_mid), hit_cnt, ctx->thread_id, g_doh_conf->local_logger, g_doh_conf->tables[TYPE_HOST].id);
        if( scan_ret > 0)
		{
			hit_cnt += scan_ret;
		}
	}

	// scan addr
	doh_addr_tfe2sapp(stream->addr, &sapp_addr);
	scan_ret = Maat_scan_proto_addr(g_doh_conf->maat, g_doh_conf->tables[TYPE_ADDR].id, &sapp_addr,
									0, result + hit_cnt, MAX_SCAN_RESULT - hit_cnt, &(ctx->scan_mid), ctx->thread_id);
	if (scan_ret > 0)
	{
        TFE_LOG_INFO(g_doh_conf->local_logger, "Scan %s, Hit addr: %s scan ret: %d policy_id: %d service: %d action: %d",
                     g_doh_conf->tables[TYPE_ADDR].name, stream->str_stream_info, scan_ret, result[hit_cnt].config_id, result[hit_cnt].service_id, result[hit_cnt].action);
        hit_cnt += scan_ret;
	}
	else
	{
        TFE_LOG_INFO(g_doh_conf->local_logger, "Scan %s, NO hit addr: %s scan ret: %d",
                     g_doh_conf->tables[TYPE_ADDR].name, stream->str_stream_info, scan_ret);
    }

	// scan appid
	scan_ret=Maat_scan_intval(g_doh_conf->maat, g_doh_conf->tables[TYPE_APPID].id, app_id, result + hit_cnt, MAX_SCAN_RESULT - hit_cnt, &(ctx->scan_mid), ctx->thread_id);
	if (scan_ret > 0)
	{
        TFE_LOG_INFO(g_doh_conf->local_logger, "Scan %s, Hit proto: %d scan ret: %d policy_id: %d service: %d action: %d addr: %s",
                     g_doh_conf->tables[TYPE_APPID].name, app_id, scan_ret, result[hit_cnt].config_id, result[hit_cnt].service_id, result[hit_cnt].action, stream->str_stream_info);
        hit_cnt += scan_ret;
	}
	else
	{
        TFE_LOG_INFO(g_doh_conf->local_logger, "Scan %s, NO hit proto: %d scan ret: %d addr: %s",
                     g_doh_conf->tables[TYPE_APPID].name, app_id, scan_ret, stream->str_stream_info);
    }

	// scan qname
	scan_ret = Maat_full_scan_string(g_doh_conf->maat, g_doh_conf->tables[TYPE_QNAME].id, CHARSET_UTF8,
									 qname, strlen(qname), result + hit_cnt, NULL, MAX_SCAN_RESULT - hit_cnt, &(ctx->scan_mid), ctx->thread_id);
	if (scan_ret > 0)
	{
        TFE_LOG_INFO(g_doh_conf->local_logger, "Scan %s, Hit domain: %s scan ret: %d qtype: %d policy_id: %d service: %d action: %d addr: %s",
                     g_doh_conf->tables[TYPE_QNAME].name, qname, scan_ret, qtype, result[hit_cnt].config_id, result[hit_cnt].service_id, result[hit_cnt].action, stream->str_stream_info);
        hit_cnt += scan_ret;
	}
	else
	{
        TFE_LOG_INFO(g_doh_conf->local_logger, "Scan %s, NO hit domain: %s scan ret: %d addr: %s",
                     g_doh_conf->tables[TYPE_QNAME].name, qname, scan_ret, stream->str_stream_info);
    }

	if (hit_cnt)
	{
		p_result = doh_fetch_rule(result, hit_cnt);
		if (p_result)
		{
			ctx->result_num = 1;
			ctx->result = ALLOC(struct Maat_rule_t, ctx->result_num);
			memcpy(ctx->result, p_result, sizeof(struct Maat_rule_t));
			doh_get_cheat_data(p_result, qtype, ctx, stream->str_stream_info);
		}
	}
}

static int doh_maat_init(const char *profile, const char *section)
{
	g_doh_conf->maat = (Maat_feather_t)tfe_bussiness_resouce_get(STATIC_MAAT);
	MESA_load_profile_string_def(profile, section, "table_appid", g_doh_conf->tables[TYPE_APPID].name, TFE_STRING_MAX, "TSG_OBJ_APP_ID");
	MESA_load_profile_string_def(profile, section, "table_addr", g_doh_conf->tables[TYPE_ADDR].name, TFE_STRING_MAX, "TSG_SECURITY_ADDR");
	MESA_load_profile_string_def(profile, section, "table_qname", g_doh_conf->tables[TYPE_QNAME].name, TFE_STRING_MAX, "TSG_FIELD_DOH_QNAME");
	MESA_load_profile_string_def(profile, section, "table_host", g_doh_conf->tables[TYPE_HOST].name, TFE_STRING_MAX, "TSG_FIELD_HTTP_HOST");

	for (int i = 0; i < TYPE_MAX; i++)
	{
		g_doh_conf->tables[i].id = Maat_table_register(g_doh_conf->maat, g_doh_conf->tables[i].name);
		if (g_doh_conf->tables[i].id < 0)
		{
			TFE_LOG_ERROR(g_doh_conf->local_logger, "Maat_table_register failed, table_name: %s", g_doh_conf->tables[i].name);
			return -1;
		}
	}

	return 0;
}

static void doh_gc_cb(evutil_socket_t fd, short what, void *arg)
{
	int i = 0;

	for (i = 0; i < DOH_STAT_MAX; i++)
	{
		FS_operate(g_doh_conf->fs_handle, g_doh_conf->fs_id[i], 0, FS_OP_SET, ATOMIC_READ(&(g_doh_conf->stat_val[i])));
	}
}

static int doh_field_init()
{
	int i = 0;
	struct timeval gc_delay = {0, 500 * 1000}; //Microseconds, we set 500 miliseconds here.
	const char *spec[DOH_STAT_MAX] = {0};

	spec[STAT_SESSION] = "doh_sess";
	spec[STAT_LOG_NUM] = "doh_log";
	spec[STAT_ACTION_HIJACK] = "doh_hijack";

	for (i = 0; i < DOH_STAT_MAX; i++)
	{
		if (spec[i] != NULL)
		{
			g_doh_conf->fs_id[i] = FS_register(g_doh_conf->fs_handle, FS_STYLE_FIELD, FS_CALC_CURRENT, spec[i]);
		}
	}
	g_doh_conf->gcev = event_new(g_doh_conf->gc_evbase, -1, EV_PERSIST, doh_gc_cb, NULL);
	evtimer_add(g_doh_conf->gcev, &gc_delay);

	return 0;
}

static struct doh_ctx *doh_ctx_new(unsigned int thread_id)
{
	struct doh_ctx *ctx = ALLOC(struct doh_ctx, 1);
	assert(ctx);

	ctx->magic_num = DOH_CTX_MAGIC_NUM;
	ctx->thread_id = (int)thread_id;
	ctx->scan_mid = NULL;
	ctx->opts_num = 0;
	ctx->opts = NULL;
	ctx->min_ttl = 0;
	ctx->doh_req = NULL;

	return ctx;
}

static void doh_ctx_free(struct doh_ctx *ctx)
{
	assert(ctx->magic_num == DOH_CTX_MAGIC_NUM);

	if (ctx->result)
	{
		free(ctx->result);
		ctx->result = NULL;
	}
	if (ctx->doh_req)
	{
		dns_free(ctx->doh_req);
		ctx->doh_req = NULL;
	}

	if (ctx->opts_num)
	{
		free(ctx->opts);
		ctx->opts = NULL;
	}

	if (ctx->http_req_body)
	{
		evbuffer_free(ctx->http_req_body);
		ctx->http_req_body = NULL;
	}

	if (ctx->asn_client)
	{
		free(ctx->asn_client);
		ctx->asn_client = NULL;
	}

	if (ctx->asn_server)
	{
		free(ctx->asn_server);
		ctx->asn_server = NULL;
	}

	if (ctx->location_client)
	{
		free(ctx->location_client);
		ctx->location_client = NULL;
	}

	if (ctx->location_server)
	{
		free(ctx->location_server);
		ctx->location_server = NULL;
	}

	FREE(&ctx);
}

static int req_session_is_doh(const struct tfe_http_session *session, struct doh_ctx *ctx)
{
	// https://tools.ietf.org/html/rfc8484

	// https://tools.ietf.org/id/draft-ietf-doh-dns-over-https-10.html
	// Registration of application/dns-message Media Type

	// https://tools.ietf.org/id/draft-ietf-doh-dns-over-https-05.html
	// Registration of application/dns-udpwireformat Media Type

	// POST
	const char *cont_type_val = tfe_http_std_field_read(session->req, TFE_HTTP_CONT_TYPE);
	if (cont_type_val && strncasecmp(cont_type_val, "application/dns-message", strlen("application/dns-message")) == 0)
	{
		ctx->type = DOH_TYPE_MESSAGE;
		return 1;
	}
	if (cont_type_val && strncasecmp(cont_type_val, "application/dns-udpwireformat", strlen("application/dns-udpwireformat")) == 0)
	{
		ctx->type = DOH_TYPE_UDPWIREFORMAT;
		return 1;
	}

	// GET
	const char *accept_type_val = tfe_http_nonstd_field_read(session->req, "Accept");
	if (accept_type_val && strncasecmp(accept_type_val, "application/dns-message", strlen("application/dns-message")) == 0)
	{
		ctx->type = DOH_TYPE_MESSAGE;
		return 1;
	}
	if (accept_type_val && strncasecmp(accept_type_val, "application/dns-udpwireformat", strlen("application/dns-udpwireformat")) == 0)
	{
		ctx->type = DOH_TYPE_UDPWIREFORMAT;
		return 1;
	}

	return 0;
}

static void doh_process_req(const struct tfe_stream *stream, const struct tfe_http_session *session, void **pme)
{
	struct doh_ctx *ctx = *(struct doh_ctx **)pme;
	struct tfe_http_half *response = NULL;
	struct tfe_http_session *to_write = NULL;

	int rsp_len;
	int rsp_size;
	char *rsp_buff = NULL;
	char cont_len_str[20] = {0};
	char max_age_str[20] = {0};

	char *req_data = (char *)evbuffer_pullup(ctx->http_req_body, -1);
	size_t req_len = evbuffer_get_length(ctx->http_req_body);

	if (req_data && req_len)
	{
		ctx->doh_req = dns_new();
		assert(ctx->doh_req);

		if (dns_parser(ctx->doh_req, req_data, req_len) == -1)
		{
			// base64_len = (str_len / 3 + 1) * 4
			int temp_size = (req_len / 3 + 1) * 4;
			char *temp = (char *)ALLOC(char, temp_size);
			int len = base64_encode(temp, temp_size - 1, req_data, req_len);
			TFE_LOG_ERROR(g_doh_conf->local_logger, "%s Doh parser request failed, PASSTHROUGH, data:%s", stream->str_stream_info, len > 0 ? temp : "");
			free(temp);
			goto end;
		}
		TFE_LOG_DEBUG(g_doh_conf->local_logger, "%s qtype %d qname:%s",
					  stream->str_stream_info, ctx->doh_req->query_question.qtype, ctx->doh_req->query_question.qname);

		if (ctx->doh_req->query_question.qtype != DNS_TYPE_A && ctx->doh_req->query_question.qtype != DNS_TYPE_AAAA)
		{
			TFE_LOG_INFO(g_doh_conf->local_logger, "%s Doh qtype not A/AAAA, PASSTHROUGH", stream->str_stream_info);
			goto end;
		}
		if (strlen((char *)ctx->doh_req->query_question.qname) == 0)
		{
			TFE_LOG_INFO(g_doh_conf->local_logger, "%s Doh qname is empty, PASSTHROUGH", stream->str_stream_info);
			goto end;
		}

		doh_maat_scan(stream, session, ctx, (char *)ctx->doh_req->query_question.qname, ctx->doh_req->query_question.qtype);
		Maat_clean_status(&(ctx->scan_mid));
		ctx->scan_mid = NULL;
		if (!ctx->opts_num)
		{
			TFE_LOG_INFO(g_doh_conf->local_logger, "%s Doh no hit answer type, PASSTHROUGH", stream->str_stream_info);
			goto end;
		}

		rsp_size = sizeof(cheat_pkt_opt_t) * ctx->opts_num * 16;
		rsp_buff = (char *)ALLOC(char, rsp_size);
		rsp_len = dns_cheat_response(ctx->doh_req, ctx->opts, ctx->opts_num, rsp_buff, rsp_size - 1);
		if (rsp_len < 0)
		{
			TFE_LOG_ERROR(g_doh_conf->local_logger, "%s Doh cheat response failed: %d, PASSTHROUGH", stream->str_stream_info, rsp_len);
			goto end;
		}

		to_write = tfe_http_session_allow_write(session);
		if (to_write == NULL)
		{
			assert(0);
		}

		ctx->manipulate = 1;
		ATOMIC_INC(&(g_doh_conf->stat_val[STAT_ACTION_HIJACK]));
        increase_redirect_policy_hit_num();

        snprintf(cont_len_str, sizeof(cont_len_str), "%d", rsp_len);
		response = tfe_http_session_response_create(to_write, 200);
		tfe_http_std_field_write(response, TFE_HTTP_CONT_LENGTH, cont_len_str);
		if (ctx->type == DOH_TYPE_MESSAGE)
			tfe_http_std_field_write(response, TFE_HTTP_CONT_TYPE, "application/dns-message");
		else
			tfe_http_std_field_write(response, TFE_HTTP_CONT_TYPE, "application/dns-udpwireformat");

		/* The assigned freshness lifetime of a DoH HTTP response MUST be less
		 * than or equal to the smallest TTL in the Answer section of the DNS
		 * response. */
		snprintf(max_age_str, sizeof(max_age_str), "max-age=%d", ctx->min_ttl);
		tfe_http_std_field_write(response, TFE_HTTP_CACHE_CONTROL, max_age_str);
		tfe_http_half_append_body(response, rsp_buff, rsp_len, 0);
		tfe_http_half_append_body(response, NULL, 0, 0);

		tfe_http_session_response_set(to_write, response);
		tfe_http_session_detach(session);
	end:
		if (rsp_buff)
		{
			free(rsp_buff);
			rsp_buff = NULL;
		}
	}
}

int doh_on_init(struct tfe_proxy *proxy)
{
	const char *profile = "./conf/doh/doh.conf";

	g_doh_conf = ALLOC(struct doh_conf, 1);
	assert(g_doh_conf);

	MESA_load_profile_int_def(profile, "doh", "enable", &(g_doh_conf->enable), 1);

	if (!g_doh_conf->enable)
	{
		TFE_LOG_INFO(NULL, "Doh disabled.");
		goto success;
	}
	TFE_LOG_INFO(NULL, "Doh enabled.");

	g_doh_conf->thread_num = tfe_proxy_get_work_thread_count();
    g_doh_conf->local_logger = MESA_create_runtime_log_handle("doh", RLOG_LV_DEBUG);

	g_doh_conf->gc_evbase = tfe_proxy_get_gc_evbase();
	g_doh_conf->fs_handle = tfe_proxy_get_fs_handle();
	if (doh_field_init() != 0)
	{
		TFE_LOG_ERROR(NULL, "Doh init field stat failed.");
		goto error;
	}

	if (doh_kafka_init(profile, g_doh_conf) != 0)
	{
		TFE_LOG_ERROR(NULL, "Doh init kafka failed.");
		goto error;
	}

	if (doh_maat_init(profile, "maat") != 0)
	{
		TFE_LOG_ERROR(NULL, "Doh init maat failed.");
		goto error;
	}
	TFE_LOG_INFO(g_doh_conf->local_logger, "Doh init success.");

success:
	return 0;

error:
	TFE_LOG_ERROR(NULL, "Doh init failed.");
	return -1;
}

void doh_on_begin(const struct tfe_stream *stream, const struct tfe_http_session *session, unsigned int thread_id, void **pme)
{
	struct doh_ctx *ctx = NULL;

	if (!g_doh_conf->enable)
	{
		return;
	}

	ctx = *(struct doh_ctx **)pme;
	assert(ctx == NULL);
	ctx = doh_ctx_new(thread_id);

	*pme = ctx;
}

// return : NO_CALL_NEXT_PLUGIN
// return : CALL_NEXT_PLUGIN
int doh_on_data(const struct tfe_stream *stream, const struct tfe_http_session *session, enum tfe_http_event events, const unsigned char *body_frag, size_t frag_size, unsigned int thread_id, void **pme)
{
	if (!g_doh_conf->enable)
	{
		return CALL_NEXT_PLUGIN;
	}

	struct doh_ctx *ctx = *(struct doh_ctx **)pme;
	assert(ctx);

	if (!session->req || !req_session_is_doh(session, ctx))
	{
		return CALL_NEXT_PLUGIN;
	}

	if (!ctx->count)
	{
		ctx->count = 1;
		ATOMIC_INC(&(g_doh_conf->stat_val[STAT_SESSION]));
		TFE_LOG_DEBUG(g_doh_conf->local_logger, "%s method:%s content-type:%s accept:%s url:%s",
					  stream->str_stream_info,
					  http_std_method_to_string(session->req->req_spec.method),
					  tfe_http_std_field_read(session->req, TFE_HTTP_CONT_TYPE),
					  tfe_http_nonstd_field_read(session->req, "Accept"),
					  session->req->req_spec.url);
	}

	// request get
	if (REQ_METHOD_IS_GET(session->req->req_spec.method) && (events & EV_HTTP_REQ_HDR))
	{
		/* https://tools.ietf.org/html/draft-ietf-doh-dns-over-https-04
		 *
		 * When using the GET method the URI path MUST contain a query parameter
         * name-value pair [QUERYPARAMETER] with the name of "ct" and a value
         * indicating the media-format used for the dns parameter.  The value
         * may either be an explicit media type (e.g. ct=application/dns-
         * udpwireformat&dns=...) or it may be empty.  An empty value indicates
         * the default application/dns-udpwireformat type (e.g. ct&dns=...).
		 *
		 * NOTE: evhttp_parse_query_str()
		 *       support "ct=x&dns=xxx"
		 *       support "ct=&dns=xxx"
		 *   not support "ct&dns=xxx"
		 * So we parser "dns=" by self.
		 */
		int len;
		u_char *temp = NULL;
		u_char *dns_data = NULL;
		const char *uri_begin = session->req->req_spec.uri;
		const char *uri_end = session->req->req_spec.uri + strlen(session->req->req_spec.uri) - 1;
		char *dns_end = NULL;
		char *dns_begin = (char *)strstr(uri_begin, "dns=");
		if (dns_begin == NULL)
		{
			goto error;
		}

		if ((dns_begin == uri_begin || *(dns_begin - 1) == '&' || *(dns_begin - 1) == '?') && dns_begin + 4 < uri_end)
		{
			dns_begin = dns_begin + 4;
			dns_end = strstr(dns_begin, "&");
			if (dns_end == NULL)
			{
				dns_end = (char *)uri_end;
			}
			else
			{
				dns_end -= 1;
			}

			if (dns_end <= dns_begin)
			{
				goto error;
			}

			dns_data = (u_char *)calloc(1, dns_end - dns_begin + 1 + 1);
			memcpy(dns_data, dns_begin, dns_end - dns_begin + 1);
		}

		temp = (u_char *)ALLOC(u_char, strlen((char *)dns_data) * 2);
		len = tfe_decode_base64url(temp, dns_data);
		if (len == 0)
		{
			TFE_LOG_ERROR(g_doh_conf->local_logger, "%s Doh base64 decode uri failed:%s, PASSTHROUGH", stream->str_stream_info, session->req->req_spec.uri);
			goto error;
		}

		assert(ctx->http_req_body == NULL);
		ctx->http_req_body = evbuffer_new();
		evbuffer_add(ctx->http_req_body, temp, len);
		doh_process_req(stream, session, pme);

	error:
		if (dns_data)
		{
			free(dns_data);
			dns_data = NULL;
		}

		if (temp)
		{
			free(temp);
			temp = NULL;
		}
	}

	// request post
	if (REQ_METHOD_IS_POST(session->req->req_spec.method))
	{
		if (events & EV_HTTP_REQ_BODY_BEGIN)
		{
			assert(ctx->http_req_body == NULL);
			ctx->http_req_body = evbuffer_new();
		}

		if (events & EV_HTTP_REQ_BODY_CONT)
		{
			evbuffer_add(ctx->http_req_body, body_frag, frag_size);
		}

		if (events & EV_HTTP_REQ_BODY_END)
		{
			doh_process_req(stream, session, pme);
		}
	}

	return NO_CALL_NEXT_PLUGIN;
}

void doh_on_end(const struct tfe_stream *stream, const struct tfe_http_session *session, unsigned int thread_id, void **pme)
{
	if (!g_doh_conf->enable)
	{
		return;
	}

	struct doh_ctx *ctx = *(struct doh_ctx **)pme;
	if (ctx->manipulate)
	{
		int ret = doh_send_log(g_doh_conf, session, stream, ctx);
		if (ret > 0)
		{
			ATOMIC_ADD(&(g_doh_conf->stat_val[STAT_LOG_NUM]), ret);
		}
	}

	doh_ctx_free(ctx);
	*pme = NULL;
}

struct tfe_plugin doh_spec = {
	.symbol = NULL,
	.type = TFE_PLUGIN_TYPE_BUSINESS,
	.on_init = doh_on_init,
	.on_deinit = NULL,
	.on_open = NULL,
	.on_data = NULL,
	.on_close = NULL,
	.on_session_begin = doh_on_begin,
	.on_session_data = doh_on_data,
	.on_session_end = doh_on_end};
TFE_PLUGIN_REGISTER(DOH, doh_spec)