Codebase list certgraph / c639fcf driver / http / http.go
c639fcf

Tree @c639fcf (Download .tar.gz)

http.go @c639fcfraw · history · blame

// Package http implements a certgraph driver for obtaining SSL certificates over https
package http

import (
	"crypto/tls"
	"fmt"
	"net"
	"net/http"
	"path"
	"time"

	"github.com/lanrat/certgraph/driver"
	"github.com/lanrat/certgraph/fingerprint"
	"github.com/lanrat/certgraph/status"
)

const driverName = "http"

func init() {
	driver.AddDriver(driverName)
}

type httpDriver struct {
	port      string
	save      bool
	savePath  string
	tlsConfig *tls.Config
	timeout   time.Duration
}

type httpCertDriver struct {
	parent       *httpDriver
	client       *http.Client
	fingerprints driver.FingerprintMap
	status       status.Map
	related      []string
	certs        map[fingerprint.Fingerprint]*driver.CertResult
}

func (c *httpCertDriver) GetFingerprints() (driver.FingerprintMap, error) {
	return c.fingerprints, nil
}

func (c *httpCertDriver) GetStatus() status.Map {
	return c.status
}

func (c *httpCertDriver) GetRelated() ([]string, error) {
	return c.related, nil
}

func (c *httpCertDriver) QueryCert(fp fingerprint.Fingerprint) (*driver.CertResult, error) {
	cert, found := c.certs[fp]
	if found {
		return cert, nil
	}
	return nil, fmt.Errorf("certificate with Fingerprint %s not found", fp.HexString())
}

// Driver creates a new SSL driver for HTTP Connections
func Driver(timeout time.Duration, savePath string) (driver.Driver, error) {
	d := new(httpDriver)
	d.port = "443"
	if len(savePath) > 0 {
		d.save = true
		d.savePath = savePath
	}
	d.timeout = timeout
	d.tlsConfig = &tls.Config{
		InsecureSkipVerify: true,
	}

	return d, nil
}

func (d *httpDriver) GetName() string {
	return driverName
}

func (d *httpDriver) newHTTPCertDriver() *httpCertDriver {
	result := &httpCertDriver{
		parent:       d,
		status:       make(status.Map),
		fingerprints: make(driver.FingerprintMap),
		certs:        make(map[fingerprint.Fingerprint]*driver.CertResult),
	}
	// set client & client.Transport separately so that dialTLS checkRedirect can be referenced
	result.client = &http.Client{
		Timeout:       d.timeout,
		CheckRedirect: result.checkRedirect,
	}
	result.client.Transport = &http.Transport{
		TLSClientConfig:       d.tlsConfig,
		TLSHandshakeTimeout:   d.timeout,
		ResponseHeaderTimeout: d.timeout,
		ExpectContinueTimeout: d.timeout,
		DialTLS:               result.dialTLS,
	}
	return result
}

// GetCert gets the certificates found for a given domain
func (d *httpDriver) QueryDomain(host string) (driver.Result, error) {
	results := d.newHTTPCertDriver()

	resp, err := results.client.Get(fmt.Sprintf("https://%s", host))
	fullStatus := status.CheckNetErr(err)
	if fullStatus != status.GOOD {
		return results, err // in some rare cases this error can be ignored
	}
	defer resp.Body.Close()

	// set final domain status
	results.status.Set(resp.Request.URL.Hostname(), status.New(status.GOOD))
	// no need to add certificate to c.certs and c.fingerprints here, handled in dialTLS method
	return results, nil
}

// only called after a redirect is detected
// req has the next request to send, via has the last requests
// not called for the first HTTP request that replied with the initial redirect
func (c *httpCertDriver) checkRedirect(req *http.Request, via []*http.Request) error {
	//fmt.Printf("Redirect %s -> %s\n", via[0].URL, req.URL)
	// set both domain's status's
	c.status.Set(via[0].URL.Hostname(), status.NewMeta(status.REDIRECT, req.URL.Hostname()))
	c.status.Set(req.URL.Hostname(), status.New(status.UNKNOWN))
	c.related = append(c.related, req.URL.Hostname())
	if len(via) >= 10 { // stop after 10 redirects
		// this stops the redirect
		return http.ErrUseLastResponse
	}
	return nil
}

func (c *httpCertDriver) dialTLS(network, addr string) (net.Conn, error) {
	dialer := &net.Dialer{Timeout: c.client.Timeout}
	conn, err := tls.DialWithDialer(dialer, network, addr, c.parent.tlsConfig)
	if conn == nil {
		return conn, err
	}
	// get certs passing by
	connState := conn.ConnectionState()

	// only look at leaf certificate which is valid for domain, rest of cert chain is ignored
	certResult := driver.NewCertResult(connState.PeerCertificates[0])
	c.certs[certResult.Fingerprint] = certResult
	host, _, err := net.SplitHostPort(addr)
	if err != nil {
		return conn, err
	}
	c.fingerprints.Add(host, certResult.Fingerprint)

	// save
	if c.parent.save && len(connState.PeerCertificates) > 0 {
		err = driver.CertsToPEMFile(connState.PeerCertificates, path.Join(c.parent.savePath, certResult.Fingerprint.HexString())+".pem")
	}

	return conn, err
}