package main

import (
	"bufio"
	"context"
	"crypto/rand"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"log"
	"mime"
	"net"
	"net/url"
	"os"
	"strings"
	"unicode/utf8"
)

type contextKey struct {
	name string
}

func (k *contextKey) String() string {
	return "gemini context value " + k.name
}

var (
	// ServerContextKey is a context key. It can be used in Gemini
	// handlers with context.WithValue to access the server that
	// started the handler. The associated value will be of type *Server.
	ServerContextKey = &contextKey{"gemini-server"}

	// LocalAddrContextKey is a context key. It can be used in
	// Gemini handlers with context.WithValue to access the address
	// the local address the connection arrived on.
	// The associated value will be of type net.Addr.
	LocalAddrContextKey = &contextKey{"local-addr"}
)

type Server struct {
	Addr string // TCP address to listen on, ":gemini" if empty
	Port string // TCP port

	HostnameToConfig map[string]GeminiConfig //FQDN hostname to Gemini Config
}

type conn struct {
	server *Server
	C      *tls.Conn
	Cert   *x509.Certificate
}

func (s *Server) newConn(rwc *tls.Conn) *conn {
	c := &conn{
		server: s,
		C:      rwc,
	}
	return c
}

func ListenAndServeTLS(port string, cps []GeminiConfig) error {
	server := &Server{
		Addr:             ":" + port,
		Port:             port,
		HostnameToConfig: make(map[string]GeminiConfig),
	}
	for _, c := range cps {
		server.HostnameToConfig[c.Hostname] = c
	}
	return server.ListenAndServeTLS(cps)
}

func (s *Server) ListenAndServeTLS(configs []GeminiConfig) error {
	addr := s.Addr
	mime.AddExtensionType(".gmi", "text/gemini")
	mime.AddExtensionType(".gemini", "text/gemini")
	if addr == "" {
		addr = ":1965"
	}
	certs := make([]tls.Certificate, len(configs))
	for i, c := range configs {
		cert, err := tls.LoadX509KeyPair(c.CertFile, c.KeyFile)
		if err != nil {
			log.Fatalf("Error loading certs: %s", err)
		}
		certs[i] = cert
	}

	config := tls.Config{
		Certificates: certs,
		ClientAuth:   tls.RequestClientCert,
	}
	config.Rand = rand.Reader

	ln, err := tls.Listen("tcp", addr, &config)
	if err != nil {
		log.Fatalf("server: listen: %s", err)
	}

	return s.Serve(ln, &config)
}

func (s *Server) Serve(l net.Listener, conf *tls.Config) error {
	defer l.Close()

	ctx := context.Background()
	ctx = context.WithValue(ctx, ServerContextKey, s)
	ctx = context.WithValue(ctx, LocalAddrContextKey, l.Addr())

	for {
		rw, err := l.Accept()
		if err != nil {
			fmt.Errorf("error accepting new client: %s", err)
			return err
		}

		rw_tls := rw.(*tls.Conn)
		c := s.newConn(rw_tls)
		go c.serve(ctx)
	}
}

func (c *conn) serve(ctx context.Context) {
	buf := bufio.NewReader(c.C)
	data := make([]byte, 1026) //1024 for the URL, 2 for the CRLF
	count := 0
	for count < 1026 {
		if strings.Contains(string(data), "\r\n") {
			break
		}
		b, err := buf.ReadByte()
		if err != nil {
			log.Printf("WARN: Couldn't serve request: %v, %v bytes read", err.Error(), count)
			c.C.Close()
			return
		}
		data[count] = b
		count = count + 1
	}
	var res Response
	var req string
	if len(c.C.ConnectionState().PeerCertificates) > 1 {
		log.Printf("WARN: Client presented %v certificates, only using first one\n")
	}
	if len(c.C.ConnectionState().PeerCertificates) > 0 {
		c.Cert = c.C.ConnectionState().PeerCertificates[0]
	} else {
		c.Cert = nil
	}
	if !strings.Contains(string(data), "\r\n") {
		res = Response{STATUS_BAD_REQUEST, "Request too large", ""}
		req = "TOO_LONG_REQUEST"
	} else if !utf8.Valid(data) {
		res = Response{STATUS_BAD_REQUEST, "URL contains non UTF8 charcaters", ""}
	} else {
		req = string(data[:count-2])
		res = c.server.ParseRequest(req, c)
	}
	c.sendResponse(res)
	if c.Cert != nil {
		log.Printf("remote=%v status=%v request='%v' bytes=%v subject='%v' issuer='%v'", c.C.RemoteAddr(), res.Status, req, len(res.Body), c.Cert.Subject, c.Cert.Issuer)
	} else {
		log.Printf("remote=%v status=%v request='%v' bytes=%v subject='%v' issuer='%v'", c.C.RemoteAddr(), res.Status, req, len(res.Body), "Anon", "Anon")
	}
	c.C.Close()
}

func (s *Server) ParseRequest(req string, c *conn) Response {
	u, err := url.Parse(req)
	if err != nil {
		return Response{STATUS_BAD_REQUEST, "URL invalid", ""}
	}
	if u.Scheme == "" {
		u.Scheme = "gemini"
	} else if u.Scheme != "gemini" {
		return Response{STATUS_PROXY_REQUEST_REFUSED, "Proxying by Scheme not currently supported", ""}
	}
	if u.Port() != "1965" && u.Port() != "" {
		return Response{STATUS_PROXY_REQUEST_REFUSED, "Proxying by Port not currently supported", ""}
	}
	if u.Host == "" {
		return Response{STATUS_BAD_REQUEST, "Need to specify a host", ""}
	} else if _, ok := s.HostnameToConfig[u.Hostname()]; !ok {
		return Response{STATUS_PROXY_REQUEST_REFUSED, "Proxying by Hostname not currently supported", ""}
	}
	if strings.Contains(u.Path, "..") {
		return Response{STATUS_PERMANENT_FAILURE, "Dots in path, assuming bad faith.", ""}
	}

	//now check cert stuff
	capsule := s.HostnameToConfig[u.Hostname()]
	selector := capsule.RootDir + u.Path
	for _, idpath := range capsule.AccessControl.Identified {
		if strings.Contains(selector, capsule.RootDir+idpath) && c.Cert == nil {
			return Response{STATUS_CLIENT_CERTIFICATE_REQUIRED, "Please provide a client certificate", ""}
		}
	}
	for _, kpath := range capsule.AccessControl.Known {
		if strings.Contains(selector, capsule.RootDir+kpath) && c.Cert == nil {
			return Response{STATUS_TRANSIENT_CERTIFICATE_REQUESTED, "Please provide a client certificate", ""}
		}
	}
	for _, tpath := range capsule.AccessControl.Trusted {
		match := strings.Contains(selector, capsule.RootDir+tpath)
		if match && c.Cert == nil {
			return Response{STATUS_AUTHORISED_CERTIFICATE_REQUIRED, "Please provide a client certificate", ""}
		} else if match && validCert(capsule, c.Cert) != 0 {
			return Response{validCert(capsule, c.Cert), "Error", ""}
		}
	}

	fi, err := os.Stat(selector)
	switch {
	case err != nil:
		// File doesn't exist.
		return Response{STATUS_NOT_FOUND, "Couldn't find file", ""}
	case os.IsNotExist(err) || os.IsPermission(err):
		return Response{STATUS_NOT_FOUND, "File does not exist", ""}
	case isNotWorldReadable(fi):
		return Response{STATUS_TEMPORARY_FAILURE, "Unable to access file", ""}
	case fi.IsDir():
		if strings.HasSuffix(u.Path, "/") {
			return generateDirectory(selector)
		} else {
			return Response{STATUS_REDIRECT_PERMANENT, "gemini://" + u.Hostname() + u.Path + "/", ""}
		}
	default:
		// it's a file
		match := strings.Contains(selector, capsule.RootDir+capsule.CGIDir)
		if match && fi.Mode().Perm()&0111 == 0111 {
			//CGI file found
			return generateCGI(u, c)
		} else {
			//Normal file found
			return generateFile(selector)
		}
	}
}

func (c *conn) sendResponse(r Response) error {
	c.C.Write([]byte(fmt.Sprintf("%v %v\r\n", r.Status, r.Meta)))
	if r.Body != "" {
		c.C.Write([]byte(r.Body))
	}
	return nil
}

func isNotWorldReadable(file os.FileInfo) bool {
	return uint64(file.Mode().Perm())&0444 != 0444
}
