// Copyright 2015 Matthew Holt
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package certmagic automates the obtaining and renewal of TLS certificates,
// including TLS & HTTPS best practices such as robust OCSP stapling, caching,
// HTTP->HTTPS redirects, and more.
//
// Its high-level API serves your HTTP handlers over HTTPS if you simply give
// the domain name(s) and the http.Handler; CertMagic will create and run
// the HTTPS server for you, fully managing certificates during the lifetime
// of the server. Similarly, it can be used to start TLS listeners or return
// a ready-to-use tls.Config -- whatever layer you need TLS for, CertMagic
// makes it easy. See the HTTPS, Listen, and TLS functions for that.
//
// If you need more control, create a Config using New() and then call
// Manage() on the config; but you'll have to be sure to solve the HTTP
// and TLS-ALPN challenges yourself (unless you disabled them or use the
// DNS challenge) by using the provided Config.GetCertificate function
// in your tls.Config and/or Config.HTTPChallangeHandler in your HTTP
// handler.
//
// See the package's README for more instruction.
package certmagic

import (
	"crypto/tls"
	"fmt"
	"log"
	"net"
	"net/http"
	"net/url"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/xenolf/lego/certcrypto"
	"github.com/xenolf/lego/challenge"
)

// HTTPS serves mux for all domainNames using the HTTP
// and HTTPS ports, redirecting all HTTP requests to HTTPS.
//
// Calling this function signifies your acceptance to
// the CA's Subscriber Agreement and/or Terms of Service.
func HTTPS(domainNames []string, mux http.Handler) error {
	if mux == nil {
		mux = http.DefaultServeMux
	}

	cfg, err := manageWithDefaultConfig(domainNames, false)
	if err != nil {
		return err
	}

	httpWg.Add(1)
	defer httpWg.Done()

	// if we haven't made listeners yet, do so now,
	// and clean them up when all servers are done
	lnMu.Lock()
	if httpLn == nil && httpsLn == nil {
		httpLn, err = net.Listen("tcp", fmt.Sprintf(":%d", HTTPPort))
		if err != nil {
			lnMu.Unlock()
			return err
		}

		httpsLn, err = tls.Listen("tcp", fmt.Sprintf(":%d", HTTPSPort), cfg.TLSConfig())
		if err != nil {
			httpLn.Close()
			httpLn = nil
			lnMu.Unlock()
			return err
		}

		go func() {
			httpWg.Wait()
			lnMu.Lock()
			httpLn.Close()
			httpsLn.Close()
			lnMu.Unlock()
		}()
	}
	hln, hsln := httpLn, httpsLn
	lnMu.Unlock()

	httpHandler := cfg.HTTPChallengeHandler(http.HandlerFunc(httpRedirectHandler))

	log.Printf("%v Serving HTTP->HTTPS on %s and %s",
		domainNames, hln.Addr(), hsln.Addr())

	go http.Serve(hln, httpHandler)
	return http.Serve(hsln, mux)
}

func httpRedirectHandler(w http.ResponseWriter, r *http.Request) {
	toURL := "https://"

	// since we redirect to the standard HTTPS port, we
	// do not need to include it in the redirect URL
	requestHost, _, err := net.SplitHostPort(r.Host)
	if err != nil {
		requestHost = r.Host // host probably did not contain a port
	}

	toURL += requestHost
	toURL += r.URL.RequestURI()

	// get rid of this disgusting unencrypted HTTP connection 🤢
	w.Header().Set("Connection", "close")

	http.Redirect(w, r, toURL, http.StatusMovedPermanently)
}

// TLS enables management of certificates for domainNames
// and returns a valid tls.Config.
//
// Because this is a convenience function that returns
// only a tls.Config, it does not assume HTTP is being
// served on the HTTP port, so the HTTP challenge is
// disabled (no HTTPChallengeHandler is necessary).
//
// Calling this function signifies your acceptance to
// the CA's Subscriber Agreement and/or Terms of Service.
func TLS(domainNames []string) (*tls.Config, error) {
	cfg, err := manageWithDefaultConfig(domainNames, true)
	return cfg.TLSConfig(), err
}

// Listen manages certificates for domainName and returns a
// TLS listener.
//
// Because this convenience function returns only a TLS-enabled
// listener and does not presume HTTP is also being served,
// the HTTP challenge will be disabled.
//
// Calling this function signifies your acceptance to
// the CA's Subscriber Agreement and/or Terms of Service.
func Listen(domainNames []string) (net.Listener, error) {
	cfg, err := manageWithDefaultConfig(domainNames, true)
	if err != nil {
		return nil, err
	}
	return tls.Listen("tcp", fmt.Sprintf(":%d", HTTPSPort), cfg.TLSConfig())
}

// Manage obtains certificates for domainNames and keeps them
// renewed using the returned Config.
//
// You will need to ensure that you use a TLS config that gets
// certificates from this Config and that the HTTP and TLS-ALPN
// challenges can be solved. The easiest way to do this is to
// use cfg.TLSConfig() as your TLS config and to wrap your
// HTTP handler with cfg.HTTPChallengeHandler(). If you don't
// have an HTTP server, you will need to disable the HTTP
// challenge.
//
// If you already have a TLS config you want to use, you can
// simply set its GetCertificate field to cfg.GetCertificate.
//
// Calling this function signifies your acceptance to
// the CA's Subscriber Agreement and/or Terms of Service.
func Manage(domainNames []string) (cfg *Config, err error) {
	return manageWithDefaultConfig(domainNames, false)
}

// manageWithDefaultConfig returns a TLS configuration that
// is fully managed for the given names, optionally
// with the HTTP challenge disabled.
func manageWithDefaultConfig(domainNames []string, disableHTTPChallenge bool) (*Config, error) {
	cfg := NewDefault()
	cfg.DisableHTTPChallenge = disableHTTPChallenge
	return cfg, cfg.Manage(domainNames)
}

// OnDemandConfig contains some state relevant for providing
// on-demand TLS.
type OnDemandConfig struct {
	// If set, this function will be the absolute
	// authority on whether the hostname (according
	// to SNI) is allowed to try to get a cert.
	DecisionFunc func(name string) error

	// If no DecisionFunc is set, this whitelist
	// is the absolute authority as to whether
	// a certificate should be allowed to be tried.
	// Names are compared against SNI value.
	HostWhitelist []string

	// If no DecisionFunc or HostWhitelist are set,
	// then an HTTP request will be made to AskURL
	// to determine if a certificate should be
	// obtained. If the request fails or the response
	// is anything other than 2xx status code, the
	// issuance will be denied.
	AskURL *url.URL

	// If no DecisionFunc, HostWhitelist, or AskURL
	// are set, then only this many certificates may
	// be obtained on-demand; this field is required
	// if all others are empty, otherwise, all cert
	// issuances will fail.
	MaxObtain int32

	// The number of certificates that have been issued on-demand
	// by this config. It is only safe to modify this count atomically.
	// If it reaches MaxObtain, on-demand issuances must fail.
	obtainedCount int32
}

// Allowed returns whether the issuance for name is allowed according to o.
func (o *OnDemandConfig) Allowed(name string) error {
	// The decision function has absolute authority, if set
	if o.DecisionFunc != nil {
		return o.DecisionFunc(name)
	}

	// Otherwise, the host whitelist has decision authority
	if len(o.HostWhitelist) > 0 {
		return o.checkWhitelistForObtainingNewCerts(name)
	}

	// Otherwise, a URL is checked for permission to issue this cert
	if o.AskURL != nil {
		return o.checkURLForObtainingNewCerts(name)
	}

	// Otherwise use the limit defined by the "max_certs" setting
	return o.checkLimitsForObtainingNewCerts(name)
}

func (o *OnDemandConfig) whitelistContains(name string) bool {
	for _, n := range o.HostWhitelist {
		if strings.ToLower(n) == strings.ToLower(name) {
			return true
		}
	}
	return false
}

func (o *OnDemandConfig) checkWhitelistForObtainingNewCerts(name string) error {
	if !o.whitelistContains(name) {
		return fmt.Errorf("%s: name is not whitelisted", name)
	}
	return nil
}

func (o *OnDemandConfig) checkURLForObtainingNewCerts(name string) error {
	client := http.Client{
		Timeout: 10 * time.Second,
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			return fmt.Errorf("following http redirects is not allowed")
		},
	}

	// Copy the URL from the config in order to modify it for this request
	askURL := new(url.URL)
	*askURL = *o.AskURL

	query := askURL.Query()
	query.Set("domain", name)
	askURL.RawQuery = query.Encode()

	resp, err := client.Get(askURL.String())
	if err != nil {
		return fmt.Errorf("error checking %v to deterine if certificate for hostname '%s' should be allowed: %v", o.AskURL, name, err)
	}
	defer resp.Body.Close()

	if resp.StatusCode < 200 || resp.StatusCode > 299 {
		return fmt.Errorf("certificate for hostname '%s' not allowed, non-2xx status code %d returned from %v", name, resp.StatusCode, o.AskURL)
	}

	return nil
}

// checkLimitsForObtainingNewCerts checks to see if name can be issued right
// now according the maximum count defined in the configuration. If a non-nil
// error is returned, do not issue a new certificate for name.
func (o *OnDemandConfig) checkLimitsForObtainingNewCerts(name string) error {
	if o.MaxObtain == 0 {
		return fmt.Errorf("%s: no certificates allowed to be issued on-demand", name)
	}

	// User can set hard limit for number of certs for the process to issue
	if o.MaxObtain > 0 &&
		atomic.LoadInt32(&o.obtainedCount) >= o.MaxObtain {
		return fmt.Errorf("%s: maximum certificates issued (%d)", name, o.MaxObtain)
	}

	// Make sure name hasn't failed a challenge recently
	failedIssuanceMu.RLock()
	when, ok := failedIssuance[name]
	failedIssuanceMu.RUnlock()
	if ok {
		return fmt.Errorf("%s: throttled; refusing to issue cert since last attempt on %s failed", name, when.String())
	}

	// Make sure, if we've issued a few certificates already, that we haven't
	// issued any recently
	lastIssueTimeMu.Lock()
	since := time.Since(lastIssueTime)
	lastIssueTimeMu.Unlock()
	if atomic.LoadInt32(&o.obtainedCount) >= 10 && since < 10*time.Minute {
		return fmt.Errorf("%s: throttled; last certificate was obtained %v ago", name, since)
	}

	// Good to go 👍
	return nil
}

// failedIssuance is a set of names that we recently failed to get a
// certificate for from the ACME CA. They are removed after some time.
// When a name is in this map, do not issue a certificate for it on-demand.
var failedIssuance = make(map[string]time.Time)
var failedIssuanceMu sync.RWMutex

// lastIssueTime records when we last obtained a certificate successfully.
// If this value is recent, do not make any on-demand certificate requests.
var lastIssueTime time.Time
var lastIssueTimeMu sync.Mutex

// isLoopback returns true if the hostname of addr looks
// explicitly like a common local hostname. addr must only
// be a host or a host:port combination.
func isLoopback(addr string) bool {
	host, _, err := net.SplitHostPort(strings.ToLower(addr))
	if err != nil {
		host = addr // happens if the addr is only a hostname
	}
	return host == "localhost" ||
		strings.Trim(host, "[]") == "::1" ||
		strings.HasPrefix(host, "127.")
}

// isInternal returns true if the IP of addr
// belongs to a private network IP range. addr
// must only be an IP or an IP:port combination.
// Loopback addresses are considered false.
func isInternal(addr string) bool {
	privateNetworks := []string{
		"10.0.0.0/8",
		"172.16.0.0/12",
		"192.168.0.0/16",
		"fc00::/7",
	}
	host, _, err := net.SplitHostPort(addr)
	if err != nil {
		host = addr // happens if the addr is just a hostname, missing port
		// if we encounter an error, the brackets need to be stripped
		// because SplitHostPort didn't do it for us
		host = strings.Trim(host, "[]")
	}
	ip := net.ParseIP(host)
	if ip == nil {
		return false
	}
	for _, privateNetwork := range privateNetworks {
		_, ipnet, _ := net.ParseCIDR(privateNetwork)
		if ipnet.Contains(ip) {
			return true
		}
	}
	return false
}

// Package defaults
var (
	// The endpoint of the directory for the ACME
	// CA we are to use
	CA = LetsEncryptProductionCA

	// The email address to use when creating or
	// selecting an existing ACME server account
	Email string

	// The synchronization implementation - all
	// instances of certmagic in a cluster must
	// use the same value here, otherwise some
	// cert operations will not be properly
	// coordinated
	Sync Locker

	// Set to true if agreed to the CA's
	// subscriber agreement
	Agreed bool

	// Disable all HTTP challenges
	DisableHTTPChallenge bool

	// Disable all TLS-ALPN challenges
	DisableTLSALPNChallenge bool

	// How long before expiration to renew certificates
	RenewDurationBefore = DefaultRenewDurationBefore

	// How long before expiration to require a renewed
	// certificate when in interactive mode, like when
	// the program is first starting up (see
	// mholt/caddy#1680). A wider window between
	// RenewDurationBefore and this value will suppress
	// errors under duress (bad) but hopefully this duration
	// will give it enough time for the blockage to be
	// relieved.
	RenewDurationBeforeAtStartup = DefaultRenewDurationBeforeAtStartup

	// An optional event callback clients can set
	// to subscribe to certain things happening
	// internally by this config; invocations are
	// synchronous, so make them return quickly!
	OnEvent func(event string, data interface{})

	// The host (ONLY the host, not port) to listen
	// on if necessary to start a listener to solve
	// an ACME challenge
	ListenHost string

	// The alternate port to use for the ACME HTTP
	// challenge; if non-empty,  this port will be
	// used instead of HTTPChallengePort to spin up
	// a listener for the HTTP challenge
	AltHTTPPort int

	// The alternate port to use for the ACME
	// TLS-ALPN challenge; the system must forward
	// TLSALPNChallengePort to this port for
	// challenge to succeed
	AltTLSALPNPort int

	// The DNS provider to use when solving the
	// ACME DNS challenge
	DNSProvider challenge.Provider

	// The type of key to use when generating
	// certificates
	KeyType = certcrypto.RSA2048

	// The state needed to operate on-demand TLS
	OnDemand *OnDemandConfig

	// Add the must staple TLS extension to the
	// CSR generated by lego/acme
	MustStaple bool
)

const (
	// HTTPChallengePort is the officially-designated port for
	// the HTTP challenge according to the ACME spec.
	HTTPChallengePort = 80

	// TLSALPNChallengePort is the officially-designated port for
	// the TLS-ALPN challenge according to the ACME spec.
	TLSALPNChallengePort = 443
)

// Some well-known CA endpoints available to use.
const (
	LetsEncryptStagingCA    = "https://acme-staging-v02.api.letsencrypt.org/directory"
	LetsEncryptProductionCA = "https://acme-v02.api.letsencrypt.org/directory"
)

// Port variables must remain their defaults unless you
// forward packets from the defaults to whatever these
// are set to; otherwise ACME challenges will fail.
var (
	// HTTPPort is the port on which to serve HTTP
	// and, by extension, the HTTP challenge (unless
	// AltHTTPPort is set).
	HTTPPort = 80

	// HTTPSPort is the port on which to serve HTTPS
	// and, by extension, the TLS-ALPN challenge
	// (unless AltTLSALPNPort is set).
	HTTPSPort = 443
)

// Variables for conveniently serving HTTPS
var (
	httpLn, httpsLn net.Listener
	lnMu            sync.Mutex
	httpWg          sync.WaitGroup
)
