module socks

import net.ssl
import net

const socks_version5 = u8(5)

const addr_type_ipv4 = u8(1)

const addr_type_fqdn = u8(3)

const addr_type_ipv6 = u8(4)

const no_auth = u8(0)

const auth_user_password = u8(2)

// socks5_dial create new instance of &net.TcpConn
pub fn socks5_dial(proxy_url string, host string, username string, password string) !&net.TcpConn {
	mut con := net.dial_tcp(proxy_url)!
	return handshake(mut con, host, username, password)!
}

// socks5_ssl_dial create new instance of &ssl.SSLConn
pub fn socks5_ssl_dial(proxy_url string, host string, username string, password string) !&ssl.SSLConn {
	mut ssl_conn := ssl.new_ssl_conn(
		verify: ''
		cert: ''
		cert_key: ''
		validate: false
		in_memory_verification: false
	)!
	mut con := socks5_dial(proxy_url, host, username, password)!
	ssl_conn.connect(mut con, host.all_before_last(':')) or { panic(err) }
	return ssl_conn
}

fn handshake(mut con net.TcpConn, host string, username string, password string) !&net.TcpConn {
	mut v := [socks.socks_version5, 1]
	if username.len > 0 {
		v << socks.auth_user_password
	} else {
		v << socks.no_auth
	}

	con.write(v)!
	mut bf := []u8{len: 2}
	con.read(mut bf)!

	if bf[0] != socks.socks_version5 {
		con.close()!
		return error('unexpected protocol version ${bf[0]}')
	}
	if username.len == 0 {
		if bf[1] != 0 {
			con.close()!
			return error(reply(bf[1]))
		}
	}
	if username.len > 0 {
		v.clear()
		v << u8(1)
		v << u8(username.len)
		v << username.bytes()
		v << u8(password.len)
		v << password.bytes()

		con.write(v)!
		mut resp := []u8{len: 2}
		con.read(mut resp)!

		if resp[0] != 1 {
			con.close()!
			return error('server does not support user/password version 1')
		} else if resp[1] != 0 {
			con.close()!
			return error('user/password login failed')
		}
	}
	v.clear()
	v = [socks.socks_version5, 1, 0]

	mut port := host.all_after_last(':').u64()
	if port == 0 {
		port = u64(80)
	}
	address := host.all_before_last(':')

	if address.contains_only('.1234567890') { // ipv4
		v << socks.addr_type_ipv4
		v << parse_ipv4(address)!
	} else if address.contains_only(':1234567890abcdf') {
		// v << addr_type_ipv6
		// v << parse_ipv4(address)!
		// todo support ipv6
	} else { // domain
		if address.len > 255 {
			return error('${address} is too long')
		} else {
			v << socks.addr_type_fqdn
			v << u8(address.len)
			v << address.bytes()
		}
	}
	v << u8(port >> 8)
	v << u8(port)

	con.write(v)!

	mut bff := []u8{len: v.len}

	con.read(mut bff)!
	if bff[1] != 0 {
		con.close()!
		return error(reply(bff[1]))
	}
	return con
}

fn reply(code u8) string {
	match code {
		0 {
			return 'succeeded'
		}
		1 {
			return 'general SOCKS server failure'
		}
		2 {
			return 'connection not allowed by ruleset'
		}
		3 {
			return 'network unreachable'
		}
		4 {
			return 'host unreachable'
		}
		5 {
			return 'connection refused'
		}
		6 {
			return 'TTL expired'
		}
		7 {
			return 'command not supported'
		}
		8 {
			return 'address type not supported'
		}
		else {
			return 'unknown code: ${code}'
		}
	}
}

fn parse_ipv4(addr string) ![]u8 {
	mut ip := []u8{}
	for part in addr.split('.') {
		ip << part.u8()
	}

	return ip
}
