// Package tlsproxy contains a censoring TLS proxy. Most traffic is passed
// through using the SNI to choose the hostname to connect to. Specific offending
// SNIs are censored by returning a TLS alert to the client.
package tlsproxy

import (
	"context"
	"crypto/tls"
	"errors"
	"net"
	"strings"
	"sync"

	"github.com/apex/log"
)

// Dialer establishes network connections
type Dialer interface {
	DialContext(ctx context.Context, network, address string) (net.Conn, error)
}

// CensoringProxy is a censoring TLS proxy
type CensoringProxy struct {
	keywords []string
	dial     func(network, address string) (net.Conn, error)
}

// NewCensoringProxy creates a new CensoringProxy instance using
// the specified list of keywords to censor. keywords is the list
// of keywords that trigger censorship if any of them appears in
// the SNII record of a ClientHello. dnsNetwork and dnsAddress are
// settings to configure the upstream, non censored DNS.
func NewCensoringProxy(
	keywords []string, uncensored Dialer,
) *CensoringProxy {
	return &CensoringProxy{
		keywords: keywords,
		dial: func(network, address string) (net.Conn, error) {
			return uncensored.DialContext(context.Background(), network, address)
		},
	}
}

// handshakeReader is a hack to perform the initial part of the
// TLS handshake so to know the SNI and then replay the bytes of
// this initial part of the handshake with the server.
type handshakeReader struct {
	net.Conn
	incoming []byte
}

// Read saves the initial bytes of the handshake such that later
// we can replay the handshake with the real TLS server.
func (c *handshakeReader) Read(b []byte) (int, error) {
	count, err := c.Conn.Read(b)
	if err == nil {
		c.incoming = append(c.incoming, b[:count]...)
	}
	return count, err
}

// Write prevents writing on the real connection
func (c *handshakeReader) Write(b []byte) (int, error) {
	return 0, errors.New("cannot write on this connection")
}

// forward forwards left traffic to right
func forward(wg *sync.WaitGroup, left, right net.Conn) {
	data := make([]byte, 1<<18)
	for {
		n, err := left.Read(data)
		if err != nil {
			break
		}
		if _, err = right.Write(data[:n]); err != nil {
			break
		}
	}
	wg.Done()
}

// reset closes the connection with a RST segment
func reset(conn net.Conn) {
	if tc, ok := conn.(*net.TCPConn); ok {
		tc.SetLinger(0)
	}
	conn.Close()
}

// alertclose sends a TLS alert and then closes the connection
func alertclose(conn net.Conn) {
	alertdata := []byte{
		21, // alert
		3,  // version[0]
		3,  // version[1]
		0,  // length[0]
		2,  // length[1]
		2,  // fatal
		80, // internal error
	}
	conn.Write(alertdata)
	conn.Close()
}

// getsni attempts the handshakeReader hack to obtain the SNI by reading
// the beginning of the TLS handshake. On success a nonempty SNI string
// is returned. Otherwise we cannot distinguish between the absence of a
// SNI and any other reading network error that may have occurred.
func getsni(conn *handshakeReader) string {
	var (
		sni   string
		mutex sync.Mutex // just for safety
	)
	tls.Server(conn, &tls.Config{
		GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
			mutex.Lock()
			sni = info.ServerName
			mutex.Unlock()
			return nil, errors.New("tlsproxy: we can't really continue handshake")
		},
	}).Handshake()
	return sni
}

func (p *CensoringProxy) connectingToMyself(conn net.Conn) bool {
	local := conn.LocalAddr().String()
	localAddr, _, localErr := net.SplitHostPort(local)
	remote := conn.RemoteAddr().String()
	remoteAddr, _, remoteErr := net.SplitHostPort(remote)
	return localErr != nil || remoteErr != nil || localAddr == remoteAddr
}

// handle implements the TLS SNI proxy
func (p *CensoringProxy) handle(clientconn net.Conn) {
	hr := &handshakeReader{Conn: clientconn}
	sni := getsni(hr)
	if sni == "" {
		log.Warn("tlsproxy: network failure or SNI not provided")
		reset(clientconn)
		return
	}
	for _, pattern := range p.keywords {
		if strings.Contains(sni, pattern) {
			log.Warnf("tlsproxy: reject SNI by policy: %s", sni)
			alertclose(clientconn)
			return
		}
	}
	serverconn, err := p.dial("tcp", net.JoinHostPort(sni, "443"))
	if err != nil {
		log.WithError(err).Warn("tlsproxy: p.dial failed")
		alertclose(clientconn)
		return
	}
	if p.connectingToMyself(serverconn) {
		log.Warn("tlsproxy: connecting to myself")
		alertclose(clientconn)
		return
	}
	if _, err := serverconn.Write(hr.incoming); err != nil {
		log.WithError(err).Warn("tlsproxy: serverconn.Write failed")
		alertclose(clientconn)
		return
	}
	log.Debugf("tlsproxy: routing for %s", sni)
	defer clientconn.Close()
	defer serverconn.Close()
	var wg sync.WaitGroup
	wg.Add(2)
	go forward(&wg, clientconn, serverconn)
	go forward(&wg, serverconn, clientconn)
	wg.Wait()
}

func (p *CensoringProxy) run(listener net.Listener) {
	for {
		conn, err := listener.Accept()
		if err != nil && strings.Contains(
			err.Error(), "use of closed network connection") {
			return
		}
		if err == nil {
			// It's difficult to make accept fail, so restructure
			// the code such that we enter into the happy path
			go p.handle(conn)
		}
	}
}

// Start starts the censoring proxy.
func (p *CensoringProxy) Start(address string) (net.Listener, error) {
	listener, err := net.Listen("tcp", address)
	if err != nil {
		return nil, err
	}
	go p.run(listener)
	return listener, nil
}
