package socks

import (
	"errors"
	"fmt"
	"io"

	//"log"
	"net"
	//"strconv"
	"encoding/binary"
)

/*
socks5 protocol

initial

byte | 0  |   1    | 2 | ...... | n |
     |0x05|num auth|  auth methods  |


reply

byte | 0  |  1  |
     |0x05| auth|


username/password auth request

byte | 0  |  1         |          |     1 byte   |          |
     |0x01|username_len| username | password_len | password |

username/password auth reponse

byte | 0  | 1    |
     |0x01|status|

request

byte | 0  | 1 | 2  |   3    | 4 | .. | n-2 | n-1| n |
     |0x05|cmd|0x00|addrtype|      addr    |  port  |

response
byte |0   |  1   | 2  |   3    | 4 | .. | n-2 | n-1 | n |
     |0x05|status|0x00|addrtype|     addr     |  port   |

*/

// Socks5AuthRequired means socks5 server need auth or not

type socks5Conn struct {
	//addr        string
	clientConn net.Conn
	serverConn net.Conn
	dial       DialFunc
	auth       AuthService
}

func (s5 *socks5Conn) Serve(b []byte, n int) (err error) {
	defer s5.Close()

	if err = s5.handshake(b, n); err != nil {
		//log.Println(err)
		return
	}

	if err = s5.processRequest(); err != nil {
		//log.Println(err)
		return
	}
	return
}

func (s5 *socks5Conn) handshake(buf []byte, n int) (err error) {

	// read auth methods
	if n < 2 {
		n1, err := io.ReadAtLeast(s5.clientConn, buf[1:], 1)
		if err != nil {
			return err
		}
		n += n1
	}

	l := int(buf[1])
	if n != (l + 2) {
		// read remains data
		n1, err := io.ReadFull(s5.clientConn, buf[n:l+2+1])
		if err != nil {
			return err
		}
		n += n1
	}

	if s5.auth == nil {
		// no auth required
		s5.clientConn.Write([]byte{0x05, 0x00})
		return nil
	}

	hasPassAuth := false
	var passAuth byte = 0x02

	// check auth method
	// only password(0x02) supported
	for i := 2; i < n; i++ {
		if buf[i] == passAuth {
			hasPassAuth = true
			break
		}
	}

	if !hasPassAuth {
		s5.clientConn.Write([]byte{0x05, 0xff})
		return errors.New("no supported auth method")
	}

	err = s5.passwordAuth()
	return err
}

func (s5 *socks5Conn) passwordAuth() error {
	buf := make([]byte, 32)

	// username/password required
	s5.clientConn.Write([]byte{0x05, 0x02})
	n, err := io.ReadAtLeast(s5.clientConn, buf, 2)
	if err != nil {
		return err
	}

	//log.Printf("%+v", buf[:n])

	// check auth version
	if buf[0] != 0x01 {
		return errors.New("unsupported auth version")
	}

	usernameLen := int(buf[1])

	p0 := 2
	p1 := p0 + usernameLen

	for n < p1 {
		n1, err := s5.clientConn.Read(buf[n:])
		if err != nil {
			return err
		}
		n += n1
	}

	username := buf[p0:p1]
	passwordLen := int(buf[p1])

	p3 := p1 + 1
	p4 := p3 + passwordLen

	for n < p4 {
		n1, err := s5.clientConn.Read(buf[n:])
		if err != nil {
			return err
		}
		n += n1
	}

	password := buf[p3:p4]

	// log.Printf("get username: %s, password: %s", username, password)

	if s5.auth != nil {
		ret := s5.auth.Authenticate(
			string(username), string(password),
			s5.clientConn.RemoteAddr())
		if ret {
			s5.clientConn.Write([]byte{0x01, 0x00})
			return nil
		}
		s5.clientConn.Write([]byte{0x01, 0x01})

		return errors.New("access denied")
	}

	return errors.New("no auth method")
}

func (s5 *socks5Conn) processRequest() error {
	buf := make([]byte, 258)

	// read header
	n, err := io.ReadAtLeast(s5.clientConn, buf, 10)
	if err != nil {
		return err
	}

	if buf[0] != socks5Version {
		return fmt.Errorf("error version %d", buf[0])
	}

	// command only support connect
	if buf[1] != cmdConnect {
		return fmt.Errorf("unsupported command %d", buf[1])
	}

	hlen := 0   // target address length
	host := ""  // target address
	msglen := 0 // header length

	switch buf[3] {
	case addrTypeIPv4:
		hlen = 4
	case addrTypeDomain:
		hlen = int(buf[4]) + 1
	case addrTypeIPv6:
		hlen = 16
	}

	msglen = 6 + hlen

	if n < msglen {
		// read remains header
		_, err := io.ReadFull(s5.clientConn, buf[n:msglen])
		if err != nil {
			return err
		}
	}

	// get target address
	addr := buf[4 : 4+hlen]
	if buf[3] == addrTypeDomain {
		host = string(addr[1:])
	} else {
		host = net.IP(addr).String()
	}

	// get target port
	port := binary.BigEndian.Uint16(buf[msglen-2 : msglen])

	// target address
	target := net.JoinHostPort(host, fmt.Sprintf("%d", port))

	//log.Printf("connecing to %s\r\n", target)

	// connect to the target
	s5.serverConn, err = s5.dial("tcp", target)
	if err != nil {
		// connection failed
		s5.clientConn.Write([]byte{0x05, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01})
		return err
	}

	// connection success
	s5.clientConn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01})

	// enter data exchange
	forward(s5.clientConn, s5.serverConn)

	return nil
}

func (s5 *socks5Conn) Close() {
	if s5.serverConn != nil {
		s5.serverConn.Close()
	}
	if s5.clientConn != nil {
		s5.clientConn.Close()
	}
}
