// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// microproxy proxies incoming HTTPS connections to another
// destination. Instead of managing its own TLS certificates, it
// borrows issued certificates and keys from an autocert directory.
package main

import (
	"crypto/tls"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"net/http/httputil"
	"net/url"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"tailscale.com/logpolicy"
	"tailscale.com/tsweb"
)

var (
	addr          = flag.String("addr", ":4430", "server address")
	certdir       = flag.String("certdir", "", "directory to borrow LetsEncrypt certificates from")
	hostname      = flag.String("hostname", "", "hostname to serve")
	logCollection = flag.String("logcollection", "", "If non-empty, logtail collection to log to")
	nodeExporter  = flag.String("node-exporter", "http://localhost:9100", "URL of the local prometheus node exporter")
	goVarsURL     = flag.String("go-vars-url", "http://localhost:8383/debug/vars", "URL of a local Go server's /debug/vars endpoint")
	insecure      = flag.Bool("insecure", false, "serve over http, for development")
)

func main() {
	flag.Parse()

	if *logCollection != "" {
		logpolicy.New(*logCollection)
	}

	ne, err := url.Parse(*nodeExporter)
	if err != nil {
		log.Fatalf("Couldn't parse URL %q: %v", *nodeExporter, err)
	}
	proxy := httputil.NewSingleHostReverseProxy(ne)
	proxy.FlushInterval = time.Second

	if _, err = url.Parse(*goVarsURL); err != nil {
		log.Fatalf("Couldn't parse URL %q: %v", *goVarsURL, err)
	}

	mux := http.NewServeMux()
	tsweb.Debugger(mux) // registers /debug/*
	mux.Handle("/metrics", tsweb.Protected(proxy))
	mux.Handle("/varz", tsweb.Protected(tsweb.StdHandler(&goVarsHandler{*goVarsURL}, tsweb.HandlerOptions{
		Quiet200s: true,
		Logf:      log.Printf,
	})))

	ch := &certHolder{
		hostname: *hostname,
		path:     filepath.Join(*certdir, *hostname),
	}

	httpsrv := &http.Server{
		Addr:    *addr,
		Handler: mux,
	}

	if !*insecure {
		httpsrv.TLSConfig = &tls.Config{GetCertificate: ch.GetCertificate}
		err = httpsrv.ListenAndServeTLS("", "")
	} else {
		err = httpsrv.ListenAndServe()
	}
	if err != nil && err != http.ErrServerClosed {
		log.Fatal(err)
	}
}

type goVarsHandler struct {
	url string
}

func promPrint(w io.Writer, prefix string, obj map[string]interface{}) {
	for k, i := range obj {
		if prefix != "" {
			k = prefix + "_" + k
		}
		switch v := i.(type) {
		case map[string]interface{}:
			promPrint(w, k, v)
		case float64:
			const saveConfigReject = "control_save_config_rejected_"
			const saveConfig = "control_save_config_"
			switch {
			case strings.HasPrefix(k, saveConfigReject):
				fmt.Fprintf(w, "control_save_config_rejected{reason=%q} %f\n", k[len(saveConfigReject):], v)
			case strings.HasPrefix(k, saveConfig):
				fmt.Fprintf(w, "control_save_config{reason=%q} %f\n", k[len(saveConfig):], v)
			default:
				fmt.Fprintf(w, "%s %f\n", k, v)
			}
		default:
			fmt.Fprintf(w, "# Skipping key %q, unhandled type %T\n", k, v)
		}
	}
}

func (h *goVarsHandler) ServeHTTPReturn(w http.ResponseWriter, r *http.Request) error {
	resp, err := http.Get(h.url)
	if err != nil {
		return tsweb.Error(http.StatusInternalServerError, "fetch failed", err)
	}
	defer resp.Body.Close()
	var mon map[string]interface{}
	if err := json.NewDecoder(resp.Body).Decode(&mon); err != nil {
		return tsweb.Error(http.StatusInternalServerError, "fetch failed", err)
	}

	w.WriteHeader(http.StatusOK)
	promPrint(w, "", mon)
	return nil
}

// certHolder loads and caches a TLS certificate from disk, reloading
// it every hour.
type certHolder struct {
	hostname string // only hostname allowed in SNI
	path     string // path of certificate+key combined PEM file

	mu     sync.Mutex
	cert   *tls.Certificate // cached parsed cert+key
	loaded time.Time
}

func (c *certHolder) GetCertificate(ch *tls.ClientHelloInfo) (*tls.Certificate, error) {
	if ch.ServerName != c.hostname {
		return nil, fmt.Errorf("wrong client SNI %q", ch.ServerName)
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if time.Since(c.loaded) > time.Hour {
		if err := c.loadLocked(); err != nil {
			log.Printf("Reloading cert %q: %v", c.path, err)
			// continue anyway, we might be able to serve off the stale cert.
		}
	}
	return c.cert, nil
}

// load reloads the TLS certificate and key from disk. Caller must
// hold mu.
func (c *certHolder) loadLocked() error {
	bs, err := ioutil.ReadFile(c.path)
	if err != nil {
		return fmt.Errorf("reading %q: %v", c.path, err)
	}
	cert, err := tls.X509KeyPair(bs, bs)
	if err != nil {
		return fmt.Errorf("parsing %q: %v", c.path, err)
	}

	c.cert = &cert
	c.loaded = time.Now()
	return nil
}