// Package redoctober contains the server code for Red October.
//
// Copyright (c) 2013 CloudFlare, Inc.

package main

import (
	"bytes"
	"crypto/tls"
	"crypto/x509"
	"encoding/pem"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/cloudflare/redoctober/config"
	"github.com/cloudflare/redoctober/core"
	"github.com/coreos/go-systemd/activation"
	"github.com/prometheus/client_golang/prometheus"
)

// List of URLs to register and their related functions

var functions = map[string]func([]byte) ([]byte, error){
	"/create":          core.Create,
	"/create-user":     core.CreateUser,
	"/summary":         core.Summary,
	"/purge":           core.Purge,
	"/delegate":        core.Delegate,
	"/password":        core.Password,
	"/encrypt":         core.Encrypt,
	"/re-encrypt":      core.ReEncrypt,
	"/decrypt":         core.Decrypt,
	"/owners":          core.Owners,
	"/modify":          core.Modify,
	"/export":          core.Export,
	"/order":           core.Order,
	"/orderout":        core.OrdersOutstanding,
	"/orderinfo":       core.OrderInfo,
	"/ordercancel":     core.OrderCancel,
	"/restore":         core.Restore,
	"/reset-persisted": core.ResetPersisted,
	"/status":          core.Status,
}

type userRequest struct {
	rt string // The request type (which will be one of the
	// keys of the functions map above
	in []byte // Arbitrary input data (depends on the core.*
	// function called)
	resp chan<- []byte // Channel down which a response is sent (the
	// data sent will depend on the core.* function
	// called to handle this request)
}

// processRequest handles a single request receive on the JSON API for
// one of the functions named in the functions map above.
func processRequest(requestType string, w http.ResponseWriter, r *http.Request) {
	header := w.Header()
	header.Set("Content-Type", "application/json")
	header.Set("Strict-Transport-Security", "max-age=86400; includeSubDomains; preload")

	fn, ok := functions[requestType]
	if !ok {
		http.Error(w, "Unknown request", http.StatusInternalServerError)
		return
	}

	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	resp, err := fn(body)
	if err != nil {
		log.Printf("http.main failed: %s: %s", requestType, err)
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	w.Write(resp)
}

// NewServer starts an HTTPS server the handles the redoctober JSON
// API. Each of the URIs in the functions map above is setup with a
// separate HandleFunc. Each HandleFunc is an instance of queueRequest
// above.
//
// Returns a valid http.Server handling redoctober JSON requests (and
// its associated listener) or an error
func NewServer(staticPath, addr, caPath string, certPaths, keyPaths []string, useSystemdSocket bool) (*http.Server, net.Listener, error) {
	config := &tls.Config{
		PreferServerCipherSuites: true,
		SessionTicketsDisabled:   true,
	}
	for i, certPath := range certPaths {
		cert, err := tls.LoadX509KeyPair(certPath, keyPaths[i])
		if err != nil {
			return nil, nil, fmt.Errorf("Error loading certificate (%s, %s): %s", certPath, keyPaths[i], err)
		}
		config.Certificates = append(config.Certificates, cert)
	}
	config.BuildNameToCertificate()

	// If a caPath has been specified then a local CA is being used
	// and not the system configuration.

	if caPath != "" {
		pemCert, err := ioutil.ReadFile(caPath)
		if err != nil {
			return nil, nil, fmt.Errorf("Error reading %s: %s\n", caPath, err)
		}

		derCert, _ := pem.Decode(pemCert)
		if derCert == nil {
			return nil, nil, fmt.Errorf("No PEM data was found in the CA certificate file\n")
		}

		cert, err := x509.ParseCertificate(derCert.Bytes)
		if err != nil {
			return nil, nil, fmt.Errorf("Error parsing CA certificate: %s\n", err)
		}

		rootPool := x509.NewCertPool()
		rootPool.AddCert(cert)

		config.ClientAuth = tls.RequireAndVerifyClientCert
		config.ClientCAs = rootPool
	}

	var lstnr net.Listener
	if useSystemdSocket {
		listenFDs, err := activation.Listeners(true)
		if err != nil {
			log.Fatal(err)
		}
		if len(listenFDs) != 1 {
			log.Fatalf("Unexpected number of socket activation FDs! (%d)", len(listenFDs))
		}
		lstnr = tls.NewListener(listenFDs[0], config)
	} else {
		conn, err := net.Listen("tcp", addr)
		if err != nil {
			return nil, nil, fmt.Errorf("Error starting TCP listener on %s: %s\n", addr, err)
		}

		lstnr = tls.NewListener(conn, config)

	}
	mux := http.NewServeMux()

	// queue up post URIs
	for current := range functions {
		// copy this so reference does not get overwritten
		requestType := current
		mux.HandleFunc(requestType, func(w http.ResponseWriter, r *http.Request) {
			log.Printf("http.server: endpoint=%s remote=%s", requestType, r.RemoteAddr)
			processRequest(requestType, w, r)
		})
	}

	// queue up web frontend
	idxHandler := &indexHandler{staticPath}
	mux.HandleFunc("/index", idxHandler.handle)
	mux.HandleFunc("/", idxHandler.handle)

	srv := http.Server{
		Addr:         addr,
		Handler:      mux,
		TLSConfig:    config,
		TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){},
	}

	return &srv, lstnr, nil
}

type indexHandler struct {
	staticPath string
}

func (this *indexHandler) handle(w http.ResponseWriter, r *http.Request) {
	var body io.ReadSeeker
	if this.staticPath != "" {
		f, err := os.Open(this.staticPath)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		defer f.Close()
		body = f
	} else {
		body = bytes.NewReader([]byte(indexHtml))
	}

	header := w.Header()
	header.Set("Content-Type", "text/html")
	header.Set("Strict-Transport-Security", "max-age=86400; includeSubDomains; preload")
	// If the server isn't HTTPS worthy, the HSTS header won't be honored.

	http.ServeContent(w, r, "index.html", time.Now(), body)
}

// initPrometheus starts a goroutine with a Prometheus listener that
// listens on localhost:metricsPort. If the Prometheus handler can't
// be started, a log.Fatal call is made.
func initPrometheus() {
	srv := &http.Server{
		Addr:    net.JoinHostPort(cfg.Metrics.Host, cfg.Metrics.Port),
		Handler: prometheus.Handler(),
	}

	log.Printf("metrics.init start: addr=%s", srv.Addr)
	go func() {
		log.Fatal(srv.ListenAndServe())
	}()
}

const usage = `Usage:

	redoctober -static <path> -vaultpath <path> -addr <addr> -certs <path1>[,<path2>,...] -keys <path1>[,<path2>,...] [-ca <path>]

single-cert example:
redoctober -vaultpath diskrecord.json -addr localhost:8080 -certs cert.pem -keys cert.key
multi-cert example:
redoctober -vaultpath diskrecord.json -addr localhost:8080 -certs cert1.pem,cert2.pem -keys cert1.key,cert2.key
`

var (
	cfg, cli  *config.Config
	confFile  string
	vaultPath string
)

const (
	defaultAddr        = "localhost:8080"
	defaultMetricsHost = "localhost"
	defaultMetricsPort = "8081"
)

func init() {
	// cli contains the configuration set by the command line
	// options, and cfg is the actual Red October config.
	cli = config.New()
	cfg = config.New()

	cli.Server.Addr = defaultAddr
	cli.Metrics.Host = defaultMetricsHost
	cli.Metrics.Port = defaultMetricsPort

	flag.Usage = func() {
		fmt.Fprint(os.Stderr, "main usage dump\n")
		fmt.Fprint(os.Stderr, usage)
		flag.PrintDefaults()
		os.Exit(2)
	}

	flag.StringVar(&confFile, "f", "", "path to config file")
	flag.StringVar(&cli.Server.Addr, "addr", cli.Server.Addr,
		"Server and port separated by :")
	flag.StringVar(&cli.Server.CAPath, "ca", cli.Server.CAPath,
		"Path of TLS CA for client authentication (optional)")
	flag.StringVar(&cli.Server.CertPaths, "certs", cli.Server.CertPaths,
		"Path(s) of TLS certificate in PEM format, comma-separated")
	flag.StringVar(&cli.HipChat.Host, "hchost", cli.HipChat.Host,
		"Hipchat Url Base (ex: hipchat.com)")
	flag.StringVar(&cli.HipChat.APIKey, "hckey", cli.HipChat.APIKey,
		"Hipchat API Key")
	flag.StringVar(&cli.HipChat.Room, "hcroom", cli.HipChat.Room,
		"Hipchat Room ID")
	flag.StringVar(&cli.Server.KeyPaths, "keys", cli.Server.KeyPaths,
		"Comma-separated list of PEM-encoded TLS private keys in the same order as certs")
	flag.StringVar(&cli.Metrics.Host, "metrics-host", cli.Metrics.Host,
		"The `host` the metrics endpoint should listen on.")
	flag.StringVar(&cli.Metrics.Port, "metrics-port", cli.Metrics.Port,
		"The `port` the metrics endpoint should listen on.")
	flag.StringVar(&cli.UI.Root, "rohost", cli.UI.Root, "RedOctober URL Base (ex: localhost:8080)")
	flag.StringVar(&cli.UI.Static, "static", cli.UI.Static,
		"Path to override built-in index.html")
	flag.BoolVar(&cli.Server.Systemd, "systemdfds", cli.Server.Systemd,
		"Use systemd socket activation to listen on a file. Useful for binding privileged sockets.")
	flag.StringVar(&vaultPath, "vaultpath", "diskrecord.json", "Path to the the disk vault")

	flag.Parse()
}

//go:generate go run generate.go

func main() {
	var err error
	if confFile != "" {
		cfg, err = config.Load(confFile)
		if err != nil {
			log.Fatal(err)
		}
	} else {
		cfg = cli
	}

	if vaultPath == "" || !cfg.Valid() {
		if !cfg.Valid() {
			fmt.Fprintf(os.Stderr, "Invalid config.\n")
		}
		fmt.Fprint(os.Stderr, usage)
		flag.PrintDefaults()
		os.Exit(2)
	}

	if err := core.Init(vaultPath, cfg); err != nil {
		log.Fatal(err)
	}

	initPrometheus()
	cpaths := strings.Split(cfg.Server.CertPaths, ",")
	kpaths := strings.Split(cfg.Server.KeyPaths, ",")
	s, l, err := NewServer(cfg.UI.Static, cfg.Server.Addr, cfg.Server.CAPath,
		cpaths, kpaths, cfg.Server.Systemd)
	if err != nil {
		log.Fatalf("Error starting redoctober server: %s\n", err)
	}

	log.Printf("http.serve start: addr=%s", cfg.Server.Addr)
	log.Fatal(s.Serve(l))
}
