package main

import (
	"bytes"
	"crypto/tls"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"go/format"
	"io/ioutil"
	"log"
	"net/http"
	"path/filepath"
	"sort"
	"strings"

	"github.com/mozilla/tls-observatory/constants"
)

var (
	pkg        = flag.String("pkg", "rules", "package name to be added to the output file")
	outputFile = flag.String("outputFile", "tls_config.go", "name of the output file")
)

// TLSConfURL url where Mozilla publishes the TLS ciphers recommendations
const TLSConfURL = "https://statics.tls.security.mozilla.org/server-side-tls-conf.json"

// ServerSideTLSJson contains all the available configurations and the version of the current document.
type ServerSideTLSJson struct {
	Configurations map[string]Configuration `json:"configurations"`
	Version        float64                  `json:"version"`
}

// Configuration represents configurations levels declared by the Mozilla server-side-tls
// see https://wiki.mozilla.org/Security/Server_Side_TLS
type Configuration struct {
	OpenSSLCiphersuites   string   `json:"openssl_ciphersuites"`
	Ciphersuites          []string `json:"ciphersuites"`
	TLSVersions           []string `json:"tls_versions"`
	TLSCurves             []string `json:"tls_curves"`
	CertificateTypes      []string `json:"certificate_types"`
	CertificateCurves     []string `json:"certificate_curves"`
	CertificateSignatures []string `json:"certificate_signatures"`
	RsaKeySize            float64  `json:"rsa_key_size"`
	DHParamSize           float64  `json:"dh_param_size"`
	ECDHParamSize         float64  `json:"ecdh_param_size"`
	HstsMinAge            float64  `json:"hsts_min_age"`
	OldestClients         []string `json:"oldest_clients"`
}

type goCipherConfiguration struct {
	Name       string
	Ciphers    []string
	MinVersion string
	MaxVersion string
}

type goTLSConfiguration struct {
	cipherConfigs []goCipherConfiguration
}

// getTLSConfFromURL retrieves the json containing the TLS configurations from the specified URL.
func getTLSConfFromURL(url string) (*ServerSideTLSJson, error) {
	r, err := http.Get(url) // #nosec G107
	if err != nil {
		return nil, err
	}
	defer r.Body.Close()

	var sstls ServerSideTLSJson
	err = json.NewDecoder(r.Body).Decode(&sstls)
	if err != nil {
		return nil, err
	}

	return &sstls, nil
}

func getGoCipherConfig(name string, sstls ServerSideTLSJson) (goCipherConfiguration, error) {
	cipherConf := goCipherConfiguration{Name: strings.Title(name)}
	conf, ok := sstls.Configurations[name]
	if !ok {
		return cipherConf, fmt.Errorf("TLS configuration '%s' not found", name)
	}

	for _, cipherName := range conf.Ciphersuites {
		cipherSuite, ok := constants.CipherSuites[cipherName]
		if !ok {
			log.Printf("'%s' cipher is not available in crypto/tls package\n", cipherName)
		}
		if len(cipherSuite.IANAName) > 0 {
			cipherConf.Ciphers = append(cipherConf.Ciphers, cipherSuite.IANAName)
		}
	}

	versions := mapTLSVersions(conf.TLSVersions)
	if len(versions) > 0 {
		cipherConf.MinVersion = fmt.Sprintf("0x%04x", versions[0])
		cipherConf.MaxVersion = fmt.Sprintf("0x%04x", versions[len(versions)-1])
	} else {
		return cipherConf, fmt.Errorf("No TLS versions found for configuration '%s'", name)
	}
	return cipherConf, nil
}

func mapTLSVersions(tlsVersions []string) []int {
	var versions []int
	for _, tlsVersion := range tlsVersions {
		switch tlsVersion {
		case "TLSv1.2":
			versions = append(versions, tls.VersionTLS12)
		case "TLSv1.1":
			versions = append(versions, tls.VersionTLS11)
		case "TLSv1":
			versions = append(versions, tls.VersionTLS10)
		case "SSLv3":
			versions = append(versions, tls.VersionSSL30)
		default:
			continue
		}
	}
	sort.Ints(versions)
	return versions
}

func getGoTLSConf() (goTLSConfiguration, error) {
	sstls, err := getTLSConfFromURL(TLSConfURL)
	if err != nil || sstls == nil {
		msg := fmt.Sprintf("Could not load the Server Side TLS configuration from Mozilla's website. Check the URL: %s. Error: %v\n",
			TLSConfURL, err)
		panic(msg)
	}

	tlsConfg := goTLSConfiguration{}

	modern, err := getGoCipherConfig("modern", *sstls)
	if err != nil {
		return tlsConfg, err
	}
	tlsConfg.cipherConfigs = append(tlsConfg.cipherConfigs, modern)

	intermediate, err := getGoCipherConfig("intermediate", *sstls)
	if err != nil {
		return tlsConfg, err
	}
	tlsConfg.cipherConfigs = append(tlsConfg.cipherConfigs, intermediate)

	old, err := getGoCipherConfig("old", *sstls)
	if err != nil {
		return tlsConfg, err
	}
	tlsConfg.cipherConfigs = append(tlsConfg.cipherConfigs, old)

	return tlsConfg, nil
}

func getCurrentDir() (string, error) {
	dir := "."
	if args := flag.Args(); len(args) == 1 {
		dir = args[0]
	} else if len(args) > 1 {
		return "", errors.New("only one directory at a time")
	}
	dir, err := filepath.Abs(dir)
	if err != nil {
		return "", err
	}
	return dir, nil
}

func main() {
	dir, err := getCurrentDir()
	if err != nil {
		log.Fatalln(err)
	}
	tlsConfig, err := getGoTLSConf()
	if err != nil {
		log.Fatalln(err)
	}

	var buf bytes.Buffer
	err = generatedHeaderTmpl.Execute(&buf, *pkg)
	if err != nil {
		log.Fatalf("Failed to generate the header: %v", err)
	}
	for _, cipherConfig := range tlsConfig.cipherConfigs {
		err := generatedRuleTmpl.Execute(&buf, cipherConfig)
		if err != nil {
			log.Fatalf("Failed to generated the cipher config: %v", err)
		}
	}

	src, err := format.Source(buf.Bytes())
	if err != nil {
		log.Printf("warnings: Failed to format the code: %v", err)
		src = buf.Bytes()
	}

	outputPath := filepath.Join(dir, *outputFile)
	if err := ioutil.WriteFile(outputPath, src, 0644); err != nil {
		log.Fatalf("Writing output: %s", err)
	}
}
