// The obfsproxy command creates a SOCKS5 obfuscating proxy.
package main

import (
	"context"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io"
	"log"
	"net"
	"os"
	"os/signal"

	"0xacab.org/leap/obfsvpn"
	pt "git.torproject.org/pluggable-transports/goptlib.git"
)

const transportName = "obfs4"

type Config struct {
	NodeID     string `json:"node-id"`
	PrivateKey string `json:"private-key"`
	PublicKey  string `json:"public-key"`
	DRBGSeed   string `json:"drbg-seed"`
	IatMode    int    `json:"iat-mode"`
}

func main() {
	// Setup logging.
	logger := log.New(os.Stderr, "", log.LstdFlags)
	debug := log.New(io.Discard, "DEBUG ", log.LstdFlags)

	// Setup command line flags.
	var (
		verbose  bool
		vpnAddr  string
		cfgFile  string
		stateDir string
		addr     = "[::1]:0"
	)
	flags := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
	flags.BoolVar(&verbose, "v", verbose, "Enable verbose logging")
	flags.StringVar(&addr, "addr", addr, "The address to listen on for client connections")
	flags.StringVar(&vpnAddr, "vpn", vpnAddr, "The address of the OpenVPN server to connect to")
	flags.StringVar(&cfgFile, "c", cfgFile, "The JSON config file to load")
	flags.StringVar(&stateDir, "state", stateDir, "A directory in which to store bridge state")
	err := flags.Parse(os.Args[1:])
	if err != nil {
		logger.Fatalf("error parsing flags: %v", err)
	}

	if vpnAddr == "" {
		flags.PrintDefaults()
		logger.Fatal("must specify -vpn")
	}

	// TODO this needs to be configurable if we switch to UDP mode for OpenVPN.
	tcpVPNAddr, err := net.ResolveTCPAddr("tcp", vpnAddr)
	log.Println("target:", tcpVPNAddr)
	if err != nil {
		logger.Fatalf("error resolving VPN address: %v", err)
	}

	var cfg Config

	fd, err := os.Open(cfgFile) //#nosec G304
	log.Println("opening:", cfgFile)
	if err != nil {
		logger.Fatalf("error opening config file: %v", err)
	}
	err = json.NewDecoder(fd).Decode(&cfg)
	if err != nil {
		logger.Fatalf("error decoding config: %v", err)
	}

	// Configure logging.
	if verbose {
		debug.SetOutput(os.Stderr)
	}

	log.Println("config:", cfg)

	// Setup graceful shutdown.
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)

	// TODO pass a "mode" ? (kcp)
	listenConfig, err := obfsvpn.NewListenConfig(
		cfg.NodeID, cfg.PrivateKey, cfg.PublicKey,
		cfg.DRBGSeed,
		stateDir,
	)
	if err != nil {
		logger.Fatalf("Error creating listener from config: %v", err)
	}

	logger.Printf("DEBUG: %v", listenConfig)

	var network string
	if os.Getenv("KCP") == "1" {
		network = "kcp"
	} else {
		network = "tcp"
	}
	ln, err := listenConfig.Listen(ctx, network, addr)

	if err != nil {
		logger.Fatalf("error binding to %s: %v", addr, err)
	}

	go func() {
		<-ctx.Done()
		// Stop releases the signal handling and falls back to the default behavior,
		// so sending another interrupt will immediately terminate.
		stop()
		logger.Printf("shutting down…")
		err := ln.Close()
		if err != nil {
			logger.Printf("error closing listener: %v", err)
		}
	}()

	info := &pt.ServerInfo{
		OrAddr: tcpVPNAddr,
	}

	logger.Printf("Listening on %s…", ln.Addr())

	for {
		conn, err := ln.Accept()
		if err != nil {
			debug.Printf("error accepting connection: %v", err)
			continue
		}
		debug.Printf("accepted connection %v…", conn)
		go proxyConn(ctx, info, conn, logger, debug)
	}
}

// proxyConn is a connection to the obfs4 client that we have accepted. we will dial to the remote contained in info
func proxyConn(ctx context.Context, info *pt.ServerInfo, obfsConn net.Conn, logger, debug *log.Logger) {
	defer func() {
		err := obfsConn.Close()
		if err != nil {
			debug.Printf("Error closing connection: %v", err)
		}
	}()

	// FIXME scrub ips in other than debug mode!
	log.Println("Dialing:", info.OrAddr)
	log.Println("Obfs4 client:", obfsConn.RemoteAddr().String())

	/*
		    TODO(atanarjuat):

		    in the case of Tor, pt.DialOr returns a *net.TCPConn after dialing info.OrAddr.
		    in the vpn case (or any transparent proxy really), we do use
		    the pt.DialOr method to simply get a dialer to our upstream VPN remote.

		    keeping this terminology is a bit stupid and slightly confusing, instead
		    we could get the clearConn just by doing:

		    s, err := net.DialTCP("tcp", nil, info.ExtendedOrAddr)
		    if err != nil {
			    return nil, err
		    }

		    that is precisely what the code in ptlib is doing.

		    We also aspire at being a generic PT at some point, so perhaps it's better to keep the usage
		    of DialOr. Maybe not, and so we don't need to keep the confusing info struct.

		    Another argument in favor of doing our own dialing in here is that
		    we could have an UDP dialer. We need to think of a good way
		    to configure the server to distinguish between udp and tcp
		    upstream flows.
	*/

	// we'll refer to the connection to the usptream node as "clearConn", as opposed to the obfuscated conn.
	// but for sure openvpn or whatever protocol you wrap has its own layer of encryption :)

	clearConn, err := pt.DialOr(info, obfsConn.RemoteAddr().String(), transportName)

	if err != nil {
		logger.Printf("error dialing remote: %v", err)
		return
	}

	if err = CopyLoop(clearConn, obfsConn); err != nil {
		debug.Printf("%s - closed connection: %s", "obfsvpn", err.Error())
	} else {
		debug.Printf("%s - closed connection", "obfsvpn")
	}
}

// CopyLoop is a standard copy loop. We don't care too much who's client and
// who's server
func CopyLoop(left net.Conn, right net.Conn) error {

	fmt.Println("--> Entering copy loop.")

	if left == nil {
		fmt.Fprintln(os.Stderr, "--> Copy loop has a nil connection (left).")
		return errors.New("copy loop has a nil connection (left)")
	}

	if right == nil {
		fmt.Fprintln(os.Stderr, "--> Copy loop has a nil connection (right).")
		return errors.New("copy loop has a nil connection (right)")
	}

	// Note: right is always the pt connection.
	lockL := make(chan bool)
	lockR := make(chan bool)
	errChan := make(chan error)

	go CopyLeftToRight(left, right, lockL, errChan)
	go CopyRightToLeft(left, right, lockR, errChan)

	leftUp := true
	rightUp := true

	var copyErr error

	for leftUp || rightUp {
		select {
		case <-lockL:
			leftUp = false
		case <-lockR:
			rightUp = false
		case copyErr = <-errChan:
			log.Println("Error while copying")
		}
	}

	// XXX better to defer?
	err := left.Close()
	if err != nil {
		fmt.Fprintln(os.Stderr, "error closing left connection: ", err.Error())
	}
	err = right.Close()
	if err != nil {
		fmt.Fprintln(os.Stderr, "error closing right connection: ", err.Error())
	}

	return copyErr
}

// TODO check for data races

func CopyLeftToRight(l net.Conn, r net.Conn, ll chan bool, errChan chan error) {
	_, e := io.Copy(r, l)
	ll <- true
	if e != nil {
		errChan <- e
	}
}

func CopyRightToLeft(l net.Conn, r net.Conn, lr chan bool, errChan chan error) {
	_, e := io.Copy(l, r)
	lr <- true
	if e != nil {
		errChan <- e
	}
}
