// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

// Package blockblame blames specific firewall manufacturers for blocking Tailscale,
// by analyzing the SSL certificate presented when attempting to connect to a remote
// server.
package blockblame

import (
	"crypto/x509"
	"strings"
	"sync"

	"tailscale.com/feature/buildfeatures"
)

// VerifyCertificate checks if the given certificate c is issued by a firewall manufacturer
// that is known to block Tailscale connections. It returns true and the Manufacturer of
// the equipment if it is, or false and nil if it is not.
func VerifyCertificate(c *x509.Certificate) (m *Manufacturer, ok bool) {
	if !buildfeatures.HasDebug {
		return nil, false
	}
	for _, m := range manufacturers() {
		if m.match != nil && m.match(c) {
			return m, true
		}
	}
	return nil, false
}

// Manufacturer represents a firewall manufacturer that may be blocking Tailscale.
type Manufacturer struct {
	// Name is the name of the firewall manufacturer to be
	// mentioned in health warning messages, e.g. "Fortinet".
	Name string
	// match is a function that returns true if the given certificate looks like it might
	// be issued by this manufacturer.
	match matchFunc
}

func manufacturers() []*Manufacturer {
	manufacturersOnce.Do(func() {
		manufacturersList = []*Manufacturer{
			{
				Name:  "Aruba Networks",
				match: issuerContains("Aruba"),
			},
			{
				Name:  "Cisco",
				match: issuerContains("Cisco"),
			},
			{
				Name: "Fortinet",
				match: matchAny(
					issuerContains("Fortinet"),
					certEmail("support@fortinet.com"),
				),
			},
			{
				Name:  "Huawei",
				match: certEmail("mobile@huawei.com"),
			},
			{
				Name: "Palo Alto Networks",
				match: matchAny(
					issuerContains("Palo Alto Networks"),
					issuerContains("PAN-FW"),
				),
			},
			{
				Name:  "Sophos",
				match: issuerContains("Sophos"),
			},
			{
				Name: "Ubiquiti",
				match: matchAny(
					issuerContains("UniFi"),
					issuerContains("Ubiquiti"),
				),
			},
		}
	})
	return manufacturersList
}

var (
	manufacturersOnce sync.Once
	manufacturersList []*Manufacturer
)

type matchFunc func(*x509.Certificate) bool

func issuerContains(s string) matchFunc {
	return func(c *x509.Certificate) bool {
		return strings.Contains(strings.ToLower(c.Issuer.String()), strings.ToLower(s))
	}
}

func certEmail(v string) matchFunc {
	return func(c *x509.Certificate) bool {
		for _, email := range c.EmailAddresses {
			if strings.Contains(strings.ToLower(email), strings.ToLower(v)) {
				return true
			}
		}
		return false
	}
}

func matchAny(fs ...matchFunc) matchFunc {
	return func(c *x509.Certificate) bool {
		for _, f := range fs {
			if f(c) {
				return true
			}
		}
		return false
	}
}
