#include <GlobalConst.h>

#include <Mayaqua/Mayaqua.h>

SOCK *Internal_ProxyTcpConnect(PROXY_PARAM_IN *param, volatile bool *cancel_flag, IP *resolved_ip)
{
#ifdef OS_WIN32
	if (param->Hwnd != NULL)
	{
		return WinConnectEx3((HWND)param->Hwnd, param->Hostname, param->Port, param->Timeout, 0, NULL, NULL, NULL, NULL, false);
	}
#endif

	return ConnectEx4(param->Hostname, param->Port, param->Timeout, (bool *)cancel_flag, NULL, NULL, false, true, resolved_ip);
}

// Connect to an HTTP proxy
UINT ProxyHttpConnect(PROXY_PARAM_OUT *out, PROXY_PARAM_IN *in, volatile bool *cancel_flag)
{
	bool dummy_cancel_flag = false, use_auth = false;
	char target_hostname[MAX_HOST_NAME_LEN + 1];
	char target_hostname_port[MAX_SIZE];
	HTTP_HEADER *h;
	UINT i, ret;
	SOCK *s;
	// Validate arguments
	if (out == NULL || in == NULL || in->Port == 0 || in->TargetPort == 0 || IsEmptyStr(in->Hostname) || IsEmptyStr(in->TargetHostname))
	{
		return PROXY_ERROR_PARAMETER;
	}

	if (cancel_flag == NULL)
	{
		cancel_flag = &dummy_cancel_flag;
	}
	else if (*cancel_flag)
	{
		return PROXY_ERROR_CANCELED;
	}

	Zero(out, sizeof(PROXY_PARAM_OUT));

	// Open TCP connection to the proxy server
	s = Internal_ProxyTcpConnect(in, cancel_flag, &out->ResolvedIp);
	if (s == NULL)
	{
		return PROXY_ERROR_CONNECTION;
	}

	SetTimeout(s, MIN(PROXY_CONNECTION_TIMEOUT, (in->Timeout == 0 ? INFINITE : in->Timeout)));

	if ((IsEmptyStr(in->Username) || IsEmptyStr(in->Password)) == false)
	{
		use_auth = true;
	}

	Zero(target_hostname, sizeof(target_hostname));
	StrCpy(target_hostname, sizeof(target_hostname), in->TargetHostname);

	for (i = 0; i < StrLen(target_hostname); ++i)
	{
		if (target_hostname[i] == '/')
		{
			target_hostname[i] = 0;
		}
	}

	// Generate HTTP header
	if (IsStrIPv6Address(target_hostname))
	{
		IP ip;
		char iptmp[MAX_PATH];

		StrToIP(&ip, target_hostname);
		IPToStr(iptmp, sizeof(iptmp), &ip);

		Format(target_hostname_port, sizeof(target_hostname_port), "[%s]:%hu", iptmp, in->TargetPort);
	}
	else
	{
		Format(target_hostname_port, sizeof(target_hostname_port), "%s:%hu", target_hostname, in->TargetPort);
	}

	h = NewHttpHeader("CONNECT", target_hostname_port, "HTTP/1.0");

	if (IsEmptyStr(in->HttpCustomHeader) == false)
	{
		TOKEN_LIST *tokens = ParseToken(in->HttpCustomHeader, "\r\n");
		if (tokens != NULL)
		{
			for (i = 0; i < tokens->NumTokens; i++)
			{
				AddHttpValueStr(h, tokens->Token[i]);
			}

			FreeToken(tokens);
		}
	}

	if (GetHttpValue(h, "User-Agent") == NULL)
	{
		AddHttpValue(h, NewHttpValue("User-Agent", IsEmptyStr(in->HttpUserAgent) ? DEFAULT_USER_AGENT : in->HttpUserAgent));
	}

	if (GetHttpValue(h, "Host") == NULL)
	{
		AddHttpValue(h, NewHttpValue("Host", target_hostname));
	}

	if (GetHttpValue(h, "Content-Length") == NULL)
	{
		AddHttpValue(h, NewHttpValue("Content-Length", "0"));
	}

	if (GetHttpValue(h, "Proxy-Connection") == NULL)
	{
		AddHttpValue(h, NewHttpValue("Proxy-Connection", "Keep-Alive"));
	}

	if (GetHttpValue(h, "Pragma") == NULL)
	{
		AddHttpValue(h, NewHttpValue("Pragma", "no-cache"));
	}

	if (use_auth && GetHttpValue(h, "Proxy-Authorization") == NULL)
	{
		char auth_str[MAX_SIZE * 2], auth_b64_str[MAX_SIZE * 2];

		// Generate the authentication string
		Format(auth_str, sizeof(auth_str), "%s:%s", in->Username, in->Password);

		// Base64 encode
		Zero(auth_b64_str, sizeof(auth_b64_str));
		Encode64(auth_b64_str, auth_str);

		// Generate final string
		Format(auth_str, sizeof(auth_str), "Basic %s", auth_b64_str);

		AddHttpValue(h, NewHttpValue("Proxy-Authorization", auth_str));
	}

	// Transmission
	ret = SendHttpHeader(s, h);

	FreeHttpHeader(h);

	if (ret == false)
	{
		ret = PROXY_ERROR_DISCONNECTED;
		goto FAILURE;
	}

	if (*cancel_flag)
	{
		ret = PROXY_ERROR_CANCELED;
		goto FAILURE;
	}

	// Receive the results
	h = RecvHttpHeader(s);
	if (h == NULL)
	{
		FreeHttpHeader(h);
		ret = PROXY_ERROR_GENERIC;
		goto FAILURE;
	}

	ret = 0;
	if (StrLen(h->Method) == 8)
	{
		if (Cmp(h->Method, "HTTP/1.", 7) == 0)
		{
			ret = ToInt(h->Target);
		}
	}
	FreeHttpHeader(h);

	// Check the code
	switch (ret)
	{
	case 401:
	case 403:
	case 407:
		// Authentication failure
		ret = PROXY_ERROR_AUTHENTICATION;
		goto FAILURE;

	default:
		if ((ret / 100) == 2)
		{
			// Success
			SetTimeout(s, INFINITE);
			out->Sock = s;
			return PROXY_ERROR_SUCCESS;
		}
		else
		{
			// Unknown result
			ret = PROXY_ERROR_GENERIC;
		}
	}

FAILURE:
	Disconnect(s);
	ReleaseSock(s);
	return ret;
}

// Connect to a SOCKS5 proxy (RFC1928, RFC1929 defines username/password authentication)
UINT ProxySocks5Connect(PROXY_PARAM_OUT *out, PROXY_PARAM_IN *in, volatile bool *cancel_flag)
{
	bool dummy_cancel_flag = false;
	UCHAR tmp, recv_buf[2], *recv_buf_final;
	USHORT target_port;
	IP target_ip;
	UINT ret;
	SOCK *s;
	BUF *b;
	// Validate arguments
	if (out == NULL || in == NULL || in->Port == 0 || in->TargetPort == 0 || IsEmptyStr(in->Hostname) || IsEmptyStr(in->TargetHostname))
	{
		return PROXY_ERROR_PARAMETER;
	}

	if (cancel_flag == NULL)
	{
		cancel_flag = &dummy_cancel_flag;
	}
	else if (*cancel_flag)
	{
		return PROXY_ERROR_CANCELED;
	}

	Zero(out, sizeof(PROXY_PARAM_OUT));

	// Open TCP connection to the proxy server
	s = Internal_ProxyTcpConnect(in, cancel_flag, &out->ResolvedIp);
	if (s == NULL)
	{
		return PROXY_ERROR_CONNECTION;
	}

	SetTimeout(s, MIN(PROXY_CONNECTION_TIMEOUT, (in->Timeout == 0 ? INFINITE : in->Timeout)));

	// +----+----------+----------+
	// |VER | NMETHODS | METHODS  |
	// +----+----------+----------+
	// | 1  |    1     | 1 to 255 |
	// +----+----------+----------+
	//
	// X'00'			NO AUTHENTICATION REQUIRED
	// X'01'			GSSAPI
	// X'02'			USERNAME/PASSWORD
	// X'03' to X'7F'	IANA ASSIGNED
	// X'80' to X'FE'	RESERVED FOR PRIVATE METHODS
	// X'FF'			NO ACCEPTABLE METHOD

	b = NewBuf();
	tmp = 5;
	WriteBuf(b, &tmp, sizeof(tmp));	// SOCKS version
	tmp = 2;
	WriteBuf(b, &tmp, sizeof(tmp));	// Number of supported methods
	tmp = 0;
	WriteBuf(b, &tmp, sizeof(tmp));	// No authentication
	tmp = 2;
	WriteBuf(b, &tmp, sizeof(tmp));	// Username/password

	ret = SendAll(s, b->Buf, b->Size, false);

	if (ret == false)
	{
		FreeBuf(b);
		Debug("ProxySocks5Connect(): [Phase 1] Failed to send initial data to the server.\n");
		ret = PROXY_ERROR_DISCONNECTED;
		goto FAILURE;
	}

	// +----+--------+
	// |VER | METHOD |
	// +----+--------+
	// | 1  |   1    |
	// +----+--------+

	if (RecvAll(s, recv_buf, sizeof(recv_buf), false) == false)
	{
		FreeBuf(b);
		Debug("ProxySocks5Connect(): [Phase 1] Failed to receive initial data response from the server.\n");
		ret = PROXY_ERROR_DISCONNECTED;
		goto FAILURE;
	}

	if (recv_buf[0] != 5)
	{
		FreeBuf(b);
		Debug("ProxySocks5Connect(): [Phase 1] Unmatching version: %u.\n", recv_buf[0]);
		ret = PROXY_ERROR_VERSION;
		goto FAILURE;
	}

	ClearBuf(b);

	// Username/password authentication (RFC1929)
	if (recv_buf[1] == 2)
	{
		// +----+------+----------+------+----------+
		// |VER | ULEN |  UNAME   | PLEN |  PASSWD  |
		// +----+------+----------+------+----------+
		// | 1  |  1   | 1 to 255 |  1   | 1 to 255 |
		// +----+------+----------+------+----------+

		tmp = 1;
		WriteBuf(b, &tmp, sizeof(tmp));	// Authentication protocol version
		tmp = StrLen(in->Username);
		WriteBuf(b, &tmp, sizeof(tmp));	// Username length
		WriteBuf(b, in->Username, tmp);	// Username
		tmp = StrLen(in->Password);
		WriteBuf(b, &tmp, sizeof(tmp));	// Password length
		WriteBuf(b, in->Password, tmp);	// Password

		ret = SendAll(s, b->Buf, b->Size, false);

		ClearBuf(b);

		if (ret == false)
		{
			Debug("ProxySocks5Connect(): [Phase 1] Failed to send authentication data to the server.\n");
			ret = PROXY_ERROR_DISCONNECTED;
			goto FAILURE;
		}

		// +----+--------+
		// |VER | STATUS |
		// +----+--------+
		// | 1  |   1    |
		// +----+--------+

		if (RecvAll(s, recv_buf, sizeof(recv_buf), false) == false)
		{
			Debug("ProxySocks5Connect(): [Phase 1] Failed to receive authentication data response from the server.\n");
			ret = PROXY_ERROR_DISCONNECTED;
			goto FAILURE;
		}

		if (recv_buf[1] != 0)
		{
			Debug("ProxySocks5Connect(): [Phase 1] Authentication failure, error code sent by the server: %u.\n", recv_buf[1]);
			ret = PROXY_ERROR_AUTHENTICATION;
			goto FAILURE;
		}
	}

	// +----+-----+-------+------+----------+----------+
	// |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
	// +----+-----+-------+------+----------+----------+
	// | 1  |  1  | X'00' |  1   | Variable |    2     |
	// +----+-----+-------+------+----------+----------+
	//
	// VER				protocol version: X'05'
	// CMD
	// CONNECT			X'01'
	// BIND				X'02'
	// UDP ASSOCIATE	X'03'
	// RSV				RESERVED
	// ATYP				address type of following address
	// IP V4 address	X'01'
	// DOMAINNAME		X'03'
	// IP V6 address	X'04'
	// DST.ADDR			desired destination address
	// DST.PORT			desired destination port in network octet order

	// Prepare data to send
	tmp = 5;
	WriteBuf(b, &tmp, sizeof(tmp));	// SOCKS version
	tmp = 1;
	WriteBuf(b, &tmp, sizeof(tmp));	// Command
	tmp = 0;
	WriteBuf(b, &tmp, sizeof(tmp));	// Reserved byte

	// Convert the hostname to an IP structure (if it's an IP address)
	StrToIP(&target_ip, in->TargetHostname);

	// If the IP structure doesn't contain an IP address, the string should be an hostname
	if (IsZeroIp(&target_ip))
	{
		UCHAR dest_length = StrLen(in->TargetHostname);
		tmp = 3;
		WriteBuf(b, &tmp, sizeof(tmp));										// Destination type (hostname)
		WriteBuf(b, &dest_length, sizeof(dest_length));						// Destination hostname length
		WriteBuf(b, in->TargetHostname, dest_length);						// Destination hostname
	}
	else
	{
		if (IsIP6(&target_ip))
		{
			tmp = 4;
			WriteBuf(b, &tmp, sizeof(tmp));									// Destination type (IPv6)
			WriteBuf(b, target_ip.ipv6_addr, sizeof(target_ip.ipv6_addr));	// Destination IPv6 address
		}
		else
		{
			tmp = 1;
			WriteBuf(b, &tmp, sizeof(tmp));									// Destination type (IPv4)
			WriteBuf(b, target_ip.addr, sizeof(target_ip.addr));			// Destination IPv4 address
		}
	}

	// Convert the port in network octet order
	target_port = Endian16(in->TargetPort);
	WriteBuf(b, &target_port, sizeof(target_port));							// Destination port

	// Send data
	ret = SendAll(s, b->Buf, b->Size, false);
	FreeBuf(b);

	if (ret == false)
	{
		Debug("ProxySocks5Connect(): [Phase 2] Failed to send data to the server.\n");
		ret = PROXY_ERROR_DISCONNECTED;
		goto FAILURE;
	}

	// +----+-----+-------+------+----------+----------+
	// |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
	// +----+-----+-------+------+----------+----------+
	// | 1  |  1  | X’00’ |  1   | Variable |   2      |
	// +----+-----+-------+------+----------+----------+
	//
	// VER protocol version: X’05’
	// REP Reply field:
	// X’00’	succeeded
	// X’01’	general SOCKS server failure
	// X’02’	connection not allowed by ruleset
	// X’03’	Network unreachable
	// X’04’	Host unreachable
	// X’05’	Connection refused
	// X’06’	TTL expired
	// X’07’	Command not supported
	// X’08’	Address type not supported
	// X’09’	to X’FF’ unassigned

	// The packet sent by the server should always have the same size as the one we sent to it.
	// However, there are some implementations which send fixed values (aside from the first 2 bytes).
	// In order to support such implementations, we read the first 4 bytes in order to know the address type before trying to read the rest of the packet.
	recv_buf_final = Malloc(4);

	if (RecvAll(s, recv_buf_final, 4, false) == false)
	{
		Free(recv_buf_final);
		Debug("ProxySocks5Connect(): [Phase 2] Failed to receive response from the server.\n");
		ret = PROXY_ERROR_DISCONNECTED;
		goto FAILURE;
	}

	// We only need the first two bytes (version and response code), but we have to read the entire packet from the socket
	recv_buf[0] = recv_buf_final[0];
	recv_buf[1] = recv_buf_final[1];

	// We receive the rest of the packet by knowing the size according to the address type
	switch (recv_buf_final[3])
	{
	case 1:
		// IPv4
		recv_buf_final = ReAlloc(recv_buf_final, 6);			// 4 bytes (IPv4) + 2 bytes (port)
		ret = RecvAll(s, recv_buf_final, 6, false);
		break;
	case 4:
		// IPv6
		recv_buf_final = ReAlloc(recv_buf_final, 18);			// 16 bytes (IPv6) + 2 bytes (port)
		ret = RecvAll(s, recv_buf_final, 18, false);
		break;
	case 3:
		// Hostname
		ret = RecvAll(s, &tmp, 1, false);
		if (ret == true)
		{
			recv_buf_final = ReAlloc(recv_buf_final, tmp + 2);	// Hostname length + 2 bytes (port)
			ret = RecvAll(s, recv_buf_final, tmp + 2, false);
		}
	}

	Free(recv_buf_final);

	if (ret == false)
	{
		Debug("ProxySocks5Connect(): [Phase 2] Malformed response received from the server.\n");
		ret = PROXY_ERROR_DISCONNECTED;
		goto FAILURE;
	}

	if (recv_buf[0] != 5)
	{
		Debug("ProxySocks5Connect(): [Phase 2] Unmatching version: %u.\n", recv_buf_final[0]);
		ret = PROXY_ERROR_VERSION;
		goto FAILURE;
	}

	switch (recv_buf[1])
	{
	case 0:
		// Success
		SetTimeout(s, INFINITE);
		out->Sock = s;
		return PROXY_ERROR_SUCCESS;
	case 3:
	case 4:
	case 5:
		Debug("ProxySocks5Connect(): [Phase 2] Connection to target failed with error: %u\n", recv_buf[1]);
		ret = PROXY_ERROR_TARGET;
		goto FAILURE;
	default:
		Debug("ProxySocks5Connect(): [Phase 2] Connection failed with error: %u\n", recv_buf[1]);
		ret = PROXY_ERROR_GENERIC;
		goto FAILURE;
	}

FAILURE:
	Disconnect(s);
	ReleaseSock(s);
	return ret;
}

// Connect to a SOCKS4 proxy
UINT ProxySocks4Connect(PROXY_PARAM_OUT *out, PROXY_PARAM_IN *in, volatile bool *cancel_flag)
{
	bool dummy_cancel_flag = false;
	UCHAR tmp, recv_buf[8];
	USHORT target_port;
	IP target_ip;
	UINT ret;
	SOCK *s;
	BUF *b;
	// Validate arguments
	if (out == NULL || in == NULL || in->Port == 0 || in->TargetPort == 0 || IsEmptyStr(in->Hostname) || IsEmptyStr(in->TargetHostname))
	{
		return PROXY_ERROR_PARAMETER;
	}

	if (cancel_flag == NULL)
	{
		cancel_flag = &dummy_cancel_flag;
	}
	else if (*cancel_flag)
	{
		return PROXY_ERROR_CANCELED;
	}

	Zero(out, sizeof(PROXY_PARAM_OUT));

	// Get the IP address of the destination server
	if (GetIP(&target_ip, in->TargetHostname) == false)
	{
		return PROXY_ERROR_CONNECTION;
	}

	// Open TCP connection to the proxy server
	s = Internal_ProxyTcpConnect(in, cancel_flag, &out->ResolvedIp);
	if (s == NULL)
	{
		return PROXY_ERROR_CONNECTION;
	}

	SetTimeout(s, MIN(PROXY_CONNECTION_TIMEOUT, (in->Timeout == 0 ? INFINITE : in->Timeout)));

	// Send request packet
	b = NewBuf();
	tmp = 4;
	WriteBuf(b, &tmp, sizeof(tmp));
	tmp = 1;
	WriteBuf(b, &tmp, sizeof(tmp));
	target_port = Endian16(in->TargetPort);
	WriteBuf(b, &target_port, sizeof(target_port));
	WriteBuf(b, target_ip.addr, sizeof(target_ip.addr));
	WriteBuf(b, in->Username, StrLen(in->Username) + 1);

	ret = SendAll(s, b->Buf, b->Size, false);

	FreeBuf(b);

	if (ret == false)
	{
		ret = PROXY_ERROR_DISCONNECTED;
		goto FAILURE;
	}

	// Receive response packet
	if (RecvAll(s, recv_buf, sizeof(recv_buf), false) == false)
	{
		ret = PROXY_ERROR_DISCONNECTED;
		goto FAILURE;
	}

	if (recv_buf[0] != 0)
	{
		ret = PROXY_ERROR_GENERIC;
		goto FAILURE;
	}

	switch (recv_buf[1])
	{
	case 90:
		// Success
		SetTimeout(s, INFINITE);
		out->Sock = s;
		return PROXY_ERROR_SUCCESS;
	case 93:
		// Authentication failure
		ret = PROXY_ERROR_AUTHENTICATION;
		goto FAILURE;
	default:
		// Failed to connect to the target server
		ret = PROXY_ERROR_TARGET;
	}

FAILURE:
	Disconnect(s);
	ReleaseSock(s);
	return ret;
}
