package main

import (
	"bufio"
	"context"
	"crypto/tls"
	"crypto/x509"
	"errors"
	"flag"
	"fmt"
	"math/rand"
	"net"
	"os"
	"strings"
	"sync"
	"time"
)

const (
	defaultTLSConnectTimeout = 1 * time.Second
	defaultHandshakeDeadline = 3 * time.Second
)

var (
	// Public & free DNS servers
	PublicResolvers = []string{
		"1.1.1.1:53",     // Cloudflare
		"8.8.8.8:53",     // Google
		"64.6.64.6:53",   // Verisign
		"77.88.8.8:53",   // Yandex.DNS
		"74.82.42.42:53", // Hurricane Electric
		"1.0.0.1:53",     // Cloudflare Secondary
		"8.8.4.4:53",     // Google Secondary
		"77.88.8.1:53",   // Yandex.DNS Secondary
		// The following servers have shown to be unreliable
		//"64.6.65.6:53",      // Verisign Secondary
		//"9.9.9.9:53",         // Quad9
		//"149.112.112.112:53", // Quad9 Secondary
		//"84.200.69.80:53",    // DNS.WATCH
		//"84.200.70.40:53",    // DNS.WATCH Secondary
		//"8.26.56.26:53",      // Comodo Secure DNS
		//"8.20.247.20:53",     // Comodo Secure DNS Secondary
		//"195.46.39.39:53",    // SafeDNS
		//"195.46.39.40:53",    // SafeDNS Secondary
		//"69.195.152.204:53",  // OpenNIC
		//"216.146.35.35:53",   // Dyn
		//"216.146.36.36:53",   // Dyn Secondary
		//"37.235.1.174:53",   // FreeDNS
		//"37.235.1.177:53",   // FreeDNS Secondary
		//"156.154.70.1:53",    // Neustar
		//"156.154.71.1:53",   // Neustar Secondary
		//"91.239.100.100:53", // UncensoredDNS
		//"89.233.43.71:53",   // UncensoredDNS Secondary
		// Thought to falsely accuse researchers of malicious activity
		// "208.67.222.222:53", // OpenDNS Home
		// "208.67.220.220:53", // OpenDNS Home Secondary
		// These DNS servers have shown to send back fake answers
		//"198.101.242.72:53", // Alternate DNS
		//"23.253.163.53:53",  // Alternate DNS Secondary
	}
)

func main() {
	var connPort string
	var threads int
	var matchDomain string
	flag.StringVar(&connPort, "p", "443", "Ports to connect, separate with comma.")
	flag.IntVar(&threads, "t", 5, "Threads to use.")
	flag.StringVar(&matchDomain, "d", "", "Only get subdomains end with this domain.")
	flag.Parse()

	if len(connPort) == 0 {
		fmt.Println("Please check your ports input")
		os.Exit(0)
	}

	var ports []string
	if strings.Contains(connPort, ",") {
		ports = strings.Split(connPort, ",")

	} else {
		ports = []string{connPort}
	}

	var wg sync.WaitGroup
	jobsChan := make(chan string, threads*2)
	seen := make(map[string]bool)

	for i := 0; i < threads; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for addr := range jobsChan {
				certs := getCert(addr, ports)
				for _, c := range certs {
					// Check is it duplicated or not
					if _, ok := seen[c]; !ok {
						seen[c] = true
						// Check is it endswith matchDomain
						if len(matchDomain) > 0 {
							if strings.HasSuffix(c, matchDomain) {
								fmt.Println(c)
							}
						} else {
							fmt.Println(c)
						}
					} else {
						continue
					}

				}
			}
		}()
	}
	if flag.NArg() > 0 {
		// fetch for a single domain
		jobsChan <- flag.Arg(0)
	} else {
		sc := bufio.NewScanner(os.Stdin)
		for sc.Scan() {
			line := strings.TrimSpace(strings.ToLower(sc.Text()))
			jobsChan <- line
		}
	}
	close(jobsChan)
	wg.Wait()
}

func getCert(addr string, ports []string) []string {
	var certs []string
	for _, port := range ports {
		cfg := &tls.Config{InsecureSkipVerify: true}

		// Set the maximum time allowed for making the connection
		ctx, cancel := context.WithTimeout(context.Background(), defaultTLSConnectTimeout)
		defer cancel()

		// Obtain the connection
		conn, err := dialContext(ctx, "tcp", addr+":"+port)
		if err != nil {
			continue
		}
		defer conn.Close()

		c := tls.Client(conn, cfg)

		// Attempt to acquire the certificate chain
		errChan := make(chan error, 2)
		// This goroutine will break us out of the handshake
		time.AfterFunc(defaultHandshakeDeadline, func() {
			errChan <- errors.New("Handshake timeout")
		})
		// Be sure we do not wait too long in this attempt
		c.SetDeadline(time.Now().Add(defaultHandshakeDeadline))
		// The handshake is performed in the goroutine
		go func() {
			errChan <- c.Handshake()
		}()
		// The error channel returns handshake or timeout error
		if err = <-errChan; err != nil {
			continue
		}
		// Get the correct certificate in the chain
		certChain := c.ConnectionState().PeerCertificates
		cert := certChain[0]
		// Create the new requests from names found within the cert
		certs = append(certs, namesFromCert(cert)...)
	}
	return certs
}

func namesFromCert(cert *x509.Certificate) []string {
	var cn string

	for _, name := range cert.Subject.Names {
		oid := name.Type
		if len(oid) == 4 && oid[0] == 2 && oid[1] == 5 && oid[2] == 4 {
			if oid[3] == 3 {
				cn = fmt.Sprintf("%s", name.Value)
				break
			}
		}
	}

	var subdomains []string
	// Add the subject common name to the list of subdomain names
	commonName := removeAsteriskLabel(cn)
	if commonName != "" {
		subdomains = append(subdomains, commonName)
	}
	// Add the cert DNS names to the list of subdomain names
	for _, name := range cert.DNSNames {
		n := removeAsteriskLabel(name)
		if n != "" {
			subdomains = uniqAppend(subdomains, n)
		}
	}
	return subdomains
}

func removeAsteriskLabel(s string) string {
	var index int

	labels := strings.Split(s, ".")
	for i := len(labels) - 1; i >= 0; i-- {
		if strings.TrimSpace(labels[i]) == "*" {
			break
		}
		index = i
	}
	if index == len(labels)-1 {
		return ""
	}
	return strings.Join(labels[index:], ".")
}

func dialContext(ctx context.Context, network, address string) (net.Conn, error) {
	d := &net.Dialer{
		Resolver: &net.Resolver{
			PreferGo: true,
			Dial:     DNSDialContext,
		},
	}
	return d.DialContext(ctx, network, address)
}
func DNSDialContext(ctx context.Context, network, address string) (net.Conn, error) {
	d := &net.Dialer{}

	return d.DialContext(ctx, network, nextResolverAddress())
}

// NextResolverAddress - Requests the next server
func nextResolverAddress() string {
	resolvers := PublicResolvers
	rnd := rand.Int()
	idx := rnd % len(resolvers)
	return resolvers[idx]
}

func uniqAppend(orig []string, add ...string) []string {
	return append(orig, newUniqueElements(orig, add...)...)
}

// NewUniqueElements - Removes elements that have duplicates in the original or new elements
func newUniqueElements(orig []string, add ...string) []string {
	var n []string

	for _, av := range add {
		found := false
		s := strings.ToLower(av)

		// Check the original slice for duplicates
		for _, ov := range orig {
			if s == strings.ToLower(ov) {
				found = true
				break
			}
		}
		// Check that we didn't already add it in
		if !found {
			for _, nv := range n {
				if s == nv {
					found = true
					break
				}
			}
		}
		// If no duplicates were found, add the entry in
		if !found {
			n = append(n, s)
		}
	}
	return n
}