// Copyright 2014 ISRG.  All rights reserved
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package policy

import (
	"math/rand"
	"net"
	"regexp"
	"strings"

	"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose"
	"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/net/publicsuffix"
	"github.com/letsencrypt/boulder/Godeps/_workspace/src/gopkg.in/gorp.v1"
	"github.com/letsencrypt/boulder/core"
	blog "github.com/letsencrypt/boulder/log"
)

// PolicyAuthorityImpl enforces CA policy decisions.
type PolicyAuthorityImpl struct {
	log *blog.AuditLogger
	DB  *PolicyAuthorityDatabaseImpl

	EnforceWhitelist  bool
	enabledChallenges map[string]bool
	pseudoRNG         *rand.Rand
}

// NewPolicyAuthorityImpl constructs a Policy Authority.
func NewPolicyAuthorityImpl(dbMap *gorp.DbMap, enforceWhitelist bool, challengeTypes map[string]bool) (*PolicyAuthorityImpl, error) {
	logger := blog.GetAuditLogger()
	logger.Notice("Policy Authority Starting")

	// Setup policy db
	padb, err := NewPolicyAuthorityDatabaseImpl(dbMap)
	if err != nil {
		return nil, err
	}

	pa := PolicyAuthorityImpl{
		log:               logger,
		DB:                padb,
		EnforceWhitelist:  enforceWhitelist,
		enabledChallenges: challengeTypes,
		// We don't need real randomness for this.
		pseudoRNG: rand.New(rand.NewSource(99)),
	}

	return &pa, nil
}

const (
	maxLabels = 10

	// DNS defines max label length as 63 characters. Some implementations allow
	// more, but we will be conservative.
	maxLabelLength = 63

	// This is based off maxLabels * maxLabelLength, but is also a restriction based
	// on the max size of indexed storage in the issuedNames table.
	maxDNSIdentifierLength = 640

	// whitelistedPartnerRegID is the registartion ID we check for to see if we need
	// to skip the domain whitelist (but not the blacklist). This is for an
	// early partner integration during the beta period and should be removed
	// later.
	whitelistedPartnerRegID = 131
)

var dnsLabelRegexp = regexp.MustCompile("^[a-z0-9][a-z0-9-]{0,62}$")
var punycodeRegexp = regexp.MustCompile("^xn--")

func isDNSCharacter(ch byte) bool {
	return ('a' <= ch && ch <= 'z') ||
		('A' <= ch && ch <= 'Z') ||
		('0' <= ch && ch <= '9') ||
		ch == '.' || ch == '-'
}

// Test whether the domain name indicated by the label set is a label-wise
// suffix match for the provided suffix set.  If the `properSuffix` flag is
// set, then the name is required to not be in the suffix set (i.e., it must
// have at least one label beyond any suffix in the set).
func suffixMatch(labels []string, suffixSet map[string]bool, properSuffix bool) bool {
	for i := range labels {
		if domain := strings.Join(labels[i:], "."); suffixSet[domain] {
			// If we match on the whole domain, gate on properSuffix
			return !properSuffix || (i > 0)
		}
	}
	return false
}

var (
	errInvalidIdentifier   = core.MalformedRequestError("Invalid identifier type")
	errNonPublic           = core.MalformedRequestError("Name does not end in a public suffix")
	errICANNTLD            = core.MalformedRequestError("Name is an ICANN TLD")
	errBlacklisted         = core.MalformedRequestError("Name is blacklisted")
	errNotWhitelisted      = core.MalformedRequestError("Name is not whitelisted")
	errInvalidDNSCharacter = core.MalformedRequestError("Invalid character in DNS name")
	errNameTooLong         = core.MalformedRequestError("DNS name too long")
	errIPAddress           = core.MalformedRequestError("Issuance for IP addresses not supported")
	errTooManyLabels       = core.MalformedRequestError("DNS name has too many labels")
	errEmptyName           = core.MalformedRequestError("DNS name was empty")
	errTooFewLabels        = core.MalformedRequestError("DNS name does not have enough labels")
	errLabelTooShort       = core.MalformedRequestError("DNS label is too short")
	errLabelTooLong        = core.MalformedRequestError("DNS label is too long")
	errIDNNotSupported     = core.MalformedRequestError("Internationalized domain names (starting with xn--) not yet supported")
)

// WillingToIssue determines whether the CA is willing to issue for the provided
// identifier. It expects domains in id to be lowercase to prevent mismatched
// cases breaking queries.
//
// We place several criteria on identifiers we are willing to issue for:
//
//  * MUST self-identify as DNS identifiers
//  * MUST contain only bytes in the DNS hostname character set
//  * MUST NOT have more than maxLabels labels
//  * MUST follow the DNS hostname syntax rules in RFC 1035 and RFC 2181
//    In particular:
//    * MUST NOT contain underscores
//  * MUST NOT contain IDN labels (xn--)
//  * MUST NOT match the syntax of an IP address
//  * MUST end in a public suffix
//  * MUST have at least one label in addition to the public suffix
//  * MUST NOT be a label-wise suffix match for a name on the black list,
//    where comparison is case-independent (normalized to lower case)
//
// If WillingToIssue returns an error, it will be of type MalformedRequestError.
func (pa PolicyAuthorityImpl) WillingToIssue(id core.AcmeIdentifier, regID int64) error {
	if id.Type != core.IdentifierDNS {
		return errInvalidIdentifier
	}
	domain := id.Value

	if domain == "" {
		return errEmptyName
	}

	for _, ch := range []byte(domain) {
		if !isDNSCharacter(ch) {
			return errInvalidDNSCharacter
		}
	}

	if len(domain) > 255 {
		return errNameTooLong
	}

	if ip := net.ParseIP(domain); ip != nil {
		return errIPAddress
	}

	labels := strings.Split(domain, ".")
	if len(labels) > maxLabels {
		return errTooManyLabels
	}
	if len(labels) < 2 {
		return errTooFewLabels
	}
	for _, label := range labels {
		if len(label) < 1 {
			return errLabelTooShort
		}
		if len(label) > maxLabelLength {
			return errLabelTooLong
		}

		if !dnsLabelRegexp.MatchString(label) {
			return errInvalidDNSCharacter
		}

		if punycodeRegexp.MatchString(label) {
			return errIDNNotSupported
		}
	}

	// Names must end in an ICANN TLD, but they must not be equal to an ICANN TLD.
	icannTLD, err := publicsuffix.ICANNTLD(domain)
	if err != nil {
		return errNonPublic
	}
	if icannTLD == domain {
		return errICANNTLD
	}

	// Use the domain whitelist if the PA has been asked to. However, if the
	// registration ID is from a whitelisted partner we're allowing to register
	// any domain, they can get in, too.
	enforceWhitelist := pa.EnforceWhitelist
	if regID == whitelistedPartnerRegID {
		enforceWhitelist = false
	}

	// Require no match against blacklist and if enforceWhitelist is true
	// require domain to match a whitelist rule.
	if err := pa.DB.CheckHostLists(domain, enforceWhitelist); err != nil {
		return err
	}

	return nil
}

// ChallengesFor makes a decision of what challenges, and combinations, are
// acceptable for the given identifier.
//
// Note: Current implementation is static, but future versions may not be.
func (pa PolicyAuthorityImpl) ChallengesFor(identifier core.AcmeIdentifier, accountKey *jose.JsonWebKey) ([]core.Challenge, [][]int, error) {
	challenges := []core.Challenge{}

	// TODO(https://github.com/letsencrypt/boulder/issues/894): Remove this block
	if pa.enabledChallenges[core.ChallengeTypeSimpleHTTP] {
		challenges = append(challenges, core.SimpleHTTPChallenge(accountKey))
	}

	// TODO(https://github.com/letsencrypt/boulder/issues/894): Remove this block
	if pa.enabledChallenges[core.ChallengeTypeDVSNI] {
		challenges = append(challenges, core.DvsniChallenge(accountKey))
	}

	if pa.enabledChallenges[core.ChallengeTypeHTTP01] {
		challenges = append(challenges, core.HTTPChallenge01(accountKey))
	}

	if pa.enabledChallenges[core.ChallengeTypeTLSSNI01] {
		challenges = append(challenges, core.TLSSNIChallenge01(accountKey))
	}

	if pa.enabledChallenges[core.ChallengeTypeDNS01] {
		challenges = append(challenges, core.DNSChallenge01(accountKey))
	}

	// We shuffle the challenges and combinations to prevent ACME clients from
	// relying on the specific order that boulder returns them in.
	shuffled := make([]core.Challenge, len(challenges))
	combinations := make([][]int, len(challenges))

	for i, challIdx := range pa.pseudoRNG.Perm(len(challenges)) {
		shuffled[i] = challenges[challIdx]
		combinations[i] = []int{i}
	}

	shuffledCombos := make([][]int, len(combinations))
	for i, comboIdx := range pa.pseudoRNG.Perm(len(combinations)) {
		shuffledCombos[i] = combinations[comboIdx]
	}

	return shuffled, shuffledCombos, nil
}
