Merge remote-tracking branch 'upstream/master' into 414-va-log-redirects
Conflicts: va/validation-authority_test.go
This commit is contained in:
commit
e09f9eebf1
|
|
@ -332,6 +332,8 @@ var BadAlgorithmCSRhex = "308202aa30820192020100300d310b300906035504061302" +
|
|||
var FarFuture = time.Date(2100, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
var FarPast = time.Date(1950, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
var log = mocks.UseMockLog()
|
||||
|
||||
// CFSSL config
|
||||
const profileName = "ee"
|
||||
const caKeyFile = "../test/test-ca.key"
|
||||
|
|
|
|||
|
|
@ -71,19 +71,7 @@ func startMonitor(rpcCh *amqp.Channel, logger *blog.AuditLogger, stats statsd.St
|
|||
cmd.FailOnError(err, "Could not determine hostname")
|
||||
}
|
||||
|
||||
err = rpcCh.ExchangeDeclare(
|
||||
AmqpExchange,
|
||||
AmqpExchangeType,
|
||||
AmqpDurable,
|
||||
AmqpDeleteUnused,
|
||||
AmqpInternal,
|
||||
AmqpNoWait,
|
||||
nil)
|
||||
if err != nil {
|
||||
cmd.FailOnError(err, "Could not declare exchange")
|
||||
}
|
||||
|
||||
_, err = rpcCh.QueueDeclare(
|
||||
_, err = rpcCh.QueueDeclarePassive(
|
||||
QueueName,
|
||||
AmqpDurable,
|
||||
AmqpDeleteUnused,
|
||||
|
|
@ -91,17 +79,32 @@ func startMonitor(rpcCh *amqp.Channel, logger *blog.AuditLogger, stats statsd.St
|
|||
AmqpNoWait,
|
||||
nil)
|
||||
if err != nil {
|
||||
cmd.FailOnError(err, "Could not declare queue")
|
||||
}
|
||||
logger.Info(fmt.Sprintf("Queue %s does not exist on AMQP server, attempting to create.", QueueName))
|
||||
|
||||
err = rpcCh.QueueBind(
|
||||
QueueName,
|
||||
"#", //wildcard
|
||||
AmqpExchange,
|
||||
false,
|
||||
nil)
|
||||
if err != nil {
|
||||
cmd.FailOnError(err, "Could not bind queue")
|
||||
// Attempt to create the Queue if not exists
|
||||
_, err = rpcCh.QueueDeclare(
|
||||
QueueName,
|
||||
AmqpDurable,
|
||||
AmqpDeleteUnused,
|
||||
AmqpExclusive,
|
||||
AmqpNoWait,
|
||||
nil)
|
||||
if err != nil {
|
||||
cmd.FailOnError(err, "Could not declare queue")
|
||||
}
|
||||
|
||||
routingKey := "#" //wildcard
|
||||
|
||||
err = rpcCh.QueueBind(
|
||||
QueueName,
|
||||
routingKey,
|
||||
AmqpExchange,
|
||||
false,
|
||||
nil)
|
||||
if err != nil {
|
||||
txt := fmt.Sprintf("Could not bind to queue [%s]. NOTE: You may need to delete %s to re-trigger the bind attempt after fixing permissions, or manually bind the queue to %s.", QueueName, QueueName, routingKey)
|
||||
cmd.FailOnError(err, txt)
|
||||
}
|
||||
}
|
||||
|
||||
deliveries, err := rpcCh.Consume(
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ func main() {
|
|||
|
||||
blog.SetAuditLogger(auditlogger)
|
||||
|
||||
go cmd.DebugServer("localhost:8080")
|
||||
go cmd.DebugServer(c.Monolith.DebugAddr)
|
||||
|
||||
// Run StatsD profiling
|
||||
go cmd.ProfileCmd("Monolith", stats)
|
||||
|
|
|
|||
10
cmd/shell.go
10
cmd/shell.go
|
|
@ -79,6 +79,11 @@ type Config struct {
|
|||
|
||||
CA ca.Config
|
||||
|
||||
Monolith struct {
|
||||
// DebugAddr is the address to run the /debug handlers on.
|
||||
DebugAddr string
|
||||
}
|
||||
|
||||
RA struct {
|
||||
// DebugAddr is the address to run the /debug handlers on.
|
||||
DebugAddr string
|
||||
|
|
@ -297,6 +302,11 @@ func AmqpChannel(conf Config) (*amqp.Channel, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
err = rpc.AMQPDeclareExchange(conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conn.Channel()
|
||||
}
|
||||
|
||||
|
|
|
|||
184
core/dns.go
184
core/dns.go
|
|
@ -6,7 +6,6 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
|
|
@ -15,15 +14,6 @@ import (
|
|||
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// DNSSECError indicates an error caused by DNSSEC failing.
|
||||
type DNSSECError struct {
|
||||
}
|
||||
|
||||
// Error gives the DNSSEC failure notice.
|
||||
func (err DNSSECError) Error() string {
|
||||
return "DNSSEC validation failure"
|
||||
}
|
||||
|
||||
// DNSResolverImpl represents a resolver system
|
||||
type DNSResolverImpl struct {
|
||||
DNSClient *dns.Client
|
||||
|
|
@ -42,8 +32,16 @@ func NewDNSResolverImpl(dialTimeout time.Duration, servers []string) *DNSResolve
|
|||
}
|
||||
|
||||
// ExchangeOne performs a single DNS exchange with a randomly chosen server
|
||||
// out of the server list, returning the response, time, and error (if any)
|
||||
func (dnsResolver *DNSResolverImpl) ExchangeOne(m *dns.Msg) (rsp *dns.Msg, rtt time.Duration, err error) {
|
||||
// out of the server list, returning the response, time, and error (if any).
|
||||
// This method sets the DNSSEC OK bit on the message to true before sending
|
||||
// it to the resolver in case validation isn't the resolvers default behaviour.
|
||||
func (dnsResolver *DNSResolverImpl) ExchangeOne(hostname string, qtype uint16) (rsp *dns.Msg, rtt time.Duration, err error) {
|
||||
m := new(dns.Msg)
|
||||
// Set question type
|
||||
m.SetQuestion(dns.Fqdn(hostname), qtype)
|
||||
// Set DNSSEC OK bit for resolver
|
||||
m.SetEdns0(4096, true)
|
||||
|
||||
if len(dnsResolver.Servers) < 1 {
|
||||
err = fmt.Errorf("Not configured with at least one DNS Server")
|
||||
return
|
||||
|
|
@ -55,60 +53,25 @@ func (dnsResolver *DNSResolverImpl) ExchangeOne(m *dns.Msg) (rsp *dns.Msg, rtt t
|
|||
return dnsResolver.DNSClient.Exchange(m, chosenServer)
|
||||
}
|
||||
|
||||
// LookupDNSSEC sends the provided DNS message to a randomly chosen server (see
|
||||
// ExchangeOne) with DNSSEC enabled. If the lookup fails, this method sends a
|
||||
// clarification query to determine if it's because DNSSEC was invalid or just
|
||||
// a run-of-the-mill error. If it's because of DNSSEC, it returns ErrorDNSSEC.
|
||||
func (dnsResolver *DNSResolverImpl) LookupDNSSEC(m *dns.Msg) (*dns.Msg, time.Duration, error) {
|
||||
// Set DNSSEC OK bit
|
||||
m.SetEdns0(4096, true)
|
||||
r, rtt, err := dnsResolver.ExchangeOne(m)
|
||||
if err != nil {
|
||||
return r, rtt, err
|
||||
}
|
||||
|
||||
if r.Rcode != dns.RcodeSuccess && r.Rcode != dns.RcodeNameError && r.Rcode != dns.RcodeNXRrset {
|
||||
if r.Rcode == dns.RcodeServerFailure {
|
||||
// Re-send query with +cd to see if SERVFAIL was caused by DNSSEC
|
||||
// validation failure at the resolver
|
||||
m.CheckingDisabled = true
|
||||
checkR, _, err := dnsResolver.ExchangeOne(m)
|
||||
if err != nil {
|
||||
return r, rtt, err
|
||||
}
|
||||
|
||||
if checkR.Rcode != dns.RcodeServerFailure {
|
||||
// DNSSEC error, so we return the testable object.
|
||||
err = DNSSECError{}
|
||||
return r, rtt, err
|
||||
}
|
||||
}
|
||||
err = fmt.Errorf("Invalid response code: %d-%s", r.Rcode, dns.RcodeToString[r.Rcode])
|
||||
return r, rtt, err
|
||||
}
|
||||
|
||||
return r, rtt, err
|
||||
}
|
||||
|
||||
// LookupTXT uses a DNSSEC-enabled query to find all TXT records associated with
|
||||
// the provided hostname. If the query fails due to DNSSEC, error will be
|
||||
// set to ErrorDNSSEC.
|
||||
// LookupTXT sends a DNS query to find all TXT records associated with
|
||||
// the provided hostname.
|
||||
func (dnsResolver *DNSResolverImpl) LookupTXT(hostname string) ([]string, time.Duration, error) {
|
||||
var txt []string
|
||||
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion(dns.Fqdn(hostname), dns.TypeTXT)
|
||||
r, rtt, err := dnsResolver.LookupDNSSEC(m)
|
||||
|
||||
r, rtt, err := dnsResolver.ExchangeOne(hostname, dns.TypeTXT)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if r.Rcode != dns.RcodeSuccess {
|
||||
err = fmt.Errorf("DNS failure: %d-%s for TXT query", r.Rcode, dns.RcodeToString[r.Rcode])
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
for _, answer := range r.Answer {
|
||||
if answer.Header().Rrtype == dns.TypeTXT {
|
||||
txtRec := answer.(*dns.TXT)
|
||||
for _, field := range txtRec.Txt {
|
||||
txt = append(txt, field)
|
||||
if txtRec, ok := answer.(*dns.TXT); ok {
|
||||
for _, field := range txtRec.Txt {
|
||||
txt = append(txt, field)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -116,97 +79,92 @@ func (dnsResolver *DNSResolverImpl) LookupTXT(hostname string) ([]string, time.D
|
|||
return txt, rtt, err
|
||||
}
|
||||
|
||||
// LookupHost uses a DNSSEC-enabled query to find all A/AAAA records associated with
|
||||
// the provided hostname. If the query fails due to DNSSEC, error will be
|
||||
// set to ErrorDNSSEC.
|
||||
func (dnsResolver *DNSResolverImpl) LookupHost(hostname string) ([]net.IP, time.Duration, error) {
|
||||
// LookupHost sends a DNS query to find all A/AAAA records associated with
|
||||
// the provided hostname.
|
||||
func (dnsResolver *DNSResolverImpl) LookupHost(hostname string) ([]net.IP, time.Duration, time.Duration, error) {
|
||||
var addrs []net.IP
|
||||
var answers []dns.RR
|
||||
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion(dns.Fqdn(hostname), dns.TypeA)
|
||||
r, aRtt, err := dnsResolver.LookupDNSSEC(m)
|
||||
r, aRtt, err := dnsResolver.ExchangeOne(hostname, dns.TypeA)
|
||||
if err != nil {
|
||||
return addrs, aRtt, err
|
||||
return addrs, 0, 0, err
|
||||
}
|
||||
if r.Rcode != dns.RcodeSuccess {
|
||||
err = fmt.Errorf("DNS failure: %d-%s for A query", r.Rcode, dns.RcodeToString[r.Rcode])
|
||||
return nil, aRtt, 0, err
|
||||
}
|
||||
|
||||
answers = append(answers, r.Answer...)
|
||||
|
||||
m.SetQuestion(dns.Fqdn(hostname), dns.TypeAAAA)
|
||||
r, aaaaRtt, err := dnsResolver.LookupDNSSEC(m)
|
||||
r, aaaaRtt, err := dnsResolver.ExchangeOne(hostname, dns.TypeAAAA)
|
||||
if err != nil {
|
||||
return addrs, aRtt + aaaaRtt, err
|
||||
return addrs, aRtt, 0, err
|
||||
}
|
||||
if r.Rcode != dns.RcodeSuccess {
|
||||
err = fmt.Errorf("DNS failure: %d-%s for AAAA query", r.Rcode, dns.RcodeToString[r.Rcode])
|
||||
return nil, aRtt, aaaaRtt, err
|
||||
}
|
||||
|
||||
answers = append(answers, r.Answer...)
|
||||
|
||||
for _, answer := range answers {
|
||||
if answer.Header().Rrtype == dns.TypeA {
|
||||
a := answer.(*dns.A)
|
||||
addrs = append(addrs, a.A)
|
||||
if a, ok := answer.(*dns.A); ok {
|
||||
addrs = append(addrs, a.A)
|
||||
}
|
||||
} else if answer.Header().Rrtype == dns.TypeAAAA {
|
||||
aaaa := answer.(*dns.AAAA)
|
||||
addrs = append(addrs, aaaa.AAAA)
|
||||
if aaaa, ok := answer.(*dns.AAAA); ok {
|
||||
addrs = append(addrs, aaaa.AAAA)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return addrs, aRtt + aaaaRtt, nil
|
||||
return addrs, aRtt, aaaaRtt, nil
|
||||
}
|
||||
|
||||
// LookupCNAME uses a DNSSEC-enabled query to records for domain and returns either
|
||||
// the target, "", or a if the query fails due to DNSSEC, error will be set to
|
||||
// ErrorDNSSEC.
|
||||
func (dnsResolver *DNSResolverImpl) LookupCNAME(domain string) (string, error) {
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion(dns.Fqdn(domain), dns.TypeCNAME)
|
||||
|
||||
r, _, err := dnsResolver.LookupDNSSEC(m)
|
||||
// LookupCNAME sends a DNS query to find a CNAME record associated hostname and returns the
|
||||
// record target.
|
||||
func (dnsResolver *DNSResolverImpl) LookupCNAME(hostname string) (string, time.Duration, error) {
|
||||
r, rtt, err := dnsResolver.ExchangeOne(hostname, dns.TypeCNAME)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", 0, err
|
||||
}
|
||||
if r.Rcode != dns.RcodeSuccess {
|
||||
err = fmt.Errorf("DNS failure: %d-%s for CNAME query", r.Rcode, dns.RcodeToString[r.Rcode])
|
||||
return "", rtt, err
|
||||
}
|
||||
|
||||
for _, answer := range r.Answer {
|
||||
if cname, ok := answer.(*dns.CNAME); ok {
|
||||
return cname.Target, nil
|
||||
return cname.Target, rtt, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
return "", rtt, nil
|
||||
}
|
||||
|
||||
// LookupCAA uses a DNSSEC-enabled query to find all CAA records associated with
|
||||
// the provided hostname. If the query fails due to DNSSEC, error will be
|
||||
// set to ErrorDNSSEC.
|
||||
func (dnsResolver *DNSResolverImpl) LookupCAA(domain string, alias bool) ([]*dns.CAA, error) {
|
||||
if alias {
|
||||
// Check if there is a CNAME record for domain
|
||||
canonName, err := dnsResolver.LookupCNAME(domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if canonName == "" || canonName == domain {
|
||||
return []*dns.CAA{}, nil
|
||||
}
|
||||
domain = canonName
|
||||
}
|
||||
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion(dns.Fqdn(domain), dns.TypeCAA)
|
||||
|
||||
r, _, err := dnsResolver.LookupDNSSEC(m)
|
||||
// LookupCAA sends a DNS query to find all CAA records associated with
|
||||
// the provided hostname. If the response code from the resolver is SERVFAIL
|
||||
// an empty slice of CAA records is returned.
|
||||
func (dnsResolver *DNSResolverImpl) LookupCAA(hostname string) ([]*dns.CAA, time.Duration, error) {
|
||||
r, rtt, err := dnsResolver.ExchangeOne(hostname, dns.TypeCAA)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// On resolver validation failure, or other server failures, return empty an
|
||||
// set and no error.
|
||||
var CAAs []*dns.CAA
|
||||
if r.Rcode == dns.RcodeServerFailure {
|
||||
return CAAs, rtt, nil
|
||||
}
|
||||
|
||||
for _, answer := range r.Answer {
|
||||
if answer.Header().Rrtype == dns.TypeCAA {
|
||||
caaR, ok := answer.(*dns.CAA)
|
||||
if !ok {
|
||||
err = errors.New("Badly formatted record")
|
||||
return nil, err
|
||||
if caaR, ok := answer.(*dns.CAA); ok {
|
||||
CAAs = append(CAAs, caaR)
|
||||
}
|
||||
CAAs = append(CAAs, caaR)
|
||||
}
|
||||
}
|
||||
|
||||
return CAAs, nil
|
||||
return CAAs, rtt, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,11 @@ func mockDNSQuery(w dns.ResponseWriter, r *dns.Msg) {
|
|||
m.Compress = false
|
||||
|
||||
for _, q := range r.Question {
|
||||
if q.Name == "servfail.com." {
|
||||
m.Rcode = dns.RcodeServerFailure
|
||||
w.WriteMsg(m)
|
||||
return
|
||||
}
|
||||
switch q.Qtype {
|
||||
case dns.TypeSOA:
|
||||
record := new(dns.SOA)
|
||||
|
|
@ -42,8 +47,7 @@ func mockDNSQuery(w dns.ResponseWriter, r *dns.Msg) {
|
|||
w.WriteMsg(m)
|
||||
return
|
||||
case dns.TypeA:
|
||||
switch q.Name {
|
||||
case "cps.letsencrypt.org.":
|
||||
if q.Name == "cps.letsencrypt.org." {
|
||||
record := new(dns.A)
|
||||
record.Hdr = dns.RR_Header{Name: "cps.letsencrypt.org.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 0}
|
||||
record.A = net.ParseIP("127.0.0.1")
|
||||
|
|
@ -51,12 +55,6 @@ func mockDNSQuery(w dns.ResponseWriter, r *dns.Msg) {
|
|||
m.Answer = append(m.Answer, record)
|
||||
w.WriteMsg(m)
|
||||
return
|
||||
case "sigfail.verteiltesysteme.net.":
|
||||
if !r.CheckingDisabled {
|
||||
m.Rcode = dns.RcodeServerFailure
|
||||
}
|
||||
w.WriteMsg(m)
|
||||
return
|
||||
}
|
||||
case dns.TypeCAA:
|
||||
if q.Name == "bracewel.net." {
|
||||
|
|
@ -111,8 +109,7 @@ func TestMain(m *testing.M) {
|
|||
func TestDNSNoServers(t *testing.T) {
|
||||
obj := NewDNSResolverImpl(time.Hour, []string{})
|
||||
|
||||
m := new(dns.Msg)
|
||||
_, _, err := obj.ExchangeOne(m)
|
||||
_, _, err := obj.ExchangeOne("letsencrypt.org", dns.TypeA)
|
||||
|
||||
test.AssertError(t, err, "No servers")
|
||||
}
|
||||
|
|
@ -120,9 +117,7 @@ func TestDNSNoServers(t *testing.T) {
|
|||
func TestDNSOneServer(t *testing.T) {
|
||||
obj := NewDNSResolverImpl(time.Second*10, []string{dnsLoopbackAddr})
|
||||
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion("letsencrypt.org.", dns.TypeSOA)
|
||||
_, _, err := obj.ExchangeOne(m)
|
||||
_, _, err := obj.ExchangeOne("letsencrypt.org", dns.TypeSOA)
|
||||
|
||||
test.AssertNotError(t, err, "No message")
|
||||
}
|
||||
|
|
@ -130,9 +125,7 @@ func TestDNSOneServer(t *testing.T) {
|
|||
func TestDNSDuplicateServers(t *testing.T) {
|
||||
obj := NewDNSResolverImpl(time.Second*10, []string{dnsLoopbackAddr, dnsLoopbackAddr})
|
||||
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion("letsencrypt.org.", dns.TypeSOA)
|
||||
_, _, err := obj.ExchangeOne(m)
|
||||
_, _, err := obj.ExchangeOne("letsencrypt.org", dns.TypeSOA)
|
||||
|
||||
test.AssertNotError(t, err, "No message")
|
||||
}
|
||||
|
|
@ -143,39 +136,34 @@ func TestDNSLookupsNoServer(t *testing.T) {
|
|||
_, _, err := obj.LookupTXT("letsencrypt.org")
|
||||
test.AssertError(t, err, "No servers")
|
||||
|
||||
_, _, err = obj.LookupHost("letsencrypt.org")
|
||||
_, _, _, err = obj.LookupHost("letsencrypt.org")
|
||||
test.AssertError(t, err, "No servers")
|
||||
|
||||
_, err = obj.LookupCNAME("letsencrypt.org")
|
||||
_, _, err = obj.LookupCNAME("letsencrypt.org")
|
||||
test.AssertError(t, err, "No servers")
|
||||
|
||||
_, err = obj.LookupCAA("letsencrypt.org", false)
|
||||
_, _, err = obj.LookupCAA("letsencrypt.org")
|
||||
test.AssertError(t, err, "No servers")
|
||||
}
|
||||
|
||||
func TestDNSLookupDNSSEC(t *testing.T) {
|
||||
goodServer := NewDNSResolverImpl(time.Second*10, []string{dnsLoopbackAddr})
|
||||
func TestDNSServFail(t *testing.T) {
|
||||
obj := NewDNSResolverImpl(time.Second*10, []string{dnsLoopbackAddr})
|
||||
bad := "servfail.com"
|
||||
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion(dns.Fqdn("sigfail.verteiltesysteme.net"), dns.TypeA)
|
||||
_, _, err := obj.LookupTXT(bad)
|
||||
test.AssertError(t, err, "LookupTXT didn't return an error")
|
||||
|
||||
_, _, err := goodServer.LookupDNSSEC(m)
|
||||
test.AssertError(t, err, "DNSSEC failure")
|
||||
_, ok := err.(DNSSECError)
|
||||
fmt.Println(err)
|
||||
test.Assert(t, ok, "Should have been a DNSSECError")
|
||||
_, _, err = obj.LookupCNAME(bad)
|
||||
test.AssertError(t, err, "LookupCNAME didn't return an error")
|
||||
|
||||
m.SetQuestion(dns.Fqdn("sigok.verteiltesysteme.net"), dns.TypeA)
|
||||
_, _, _, err = obj.LookupHost(bad)
|
||||
test.AssertError(t, err, "LookupCNAME didn't return an error")
|
||||
|
||||
_, _, err = goodServer.LookupDNSSEC(m)
|
||||
test.AssertNotError(t, err, "DNSSEC should have worked")
|
||||
|
||||
badServer := NewDNSResolverImpl(time.Second*10, []string{"127.0.0.1:99"})
|
||||
|
||||
_, _, err = badServer.LookupDNSSEC(m)
|
||||
test.AssertError(t, err, "Should have failed")
|
||||
_, ok = err.(DNSSECError)
|
||||
test.Assert(t, !ok, "Shouldn't have been a DNSSECError")
|
||||
// CAA lookup ignores validation failures from the resolver for now
|
||||
// and returns an empty list of CAA records.
|
||||
emptyCaa, _, err := obj.LookupCAA(bad)
|
||||
test.Assert(t, len(emptyCaa) == 0, "Query returned non-empty list of CAA records")
|
||||
test.AssertNotError(t, err, "LookupCAA returned an error")
|
||||
}
|
||||
|
||||
func TestDNSLookupTXT(t *testing.T) {
|
||||
|
|
@ -190,30 +178,30 @@ func TestDNSLookupTXT(t *testing.T) {
|
|||
func TestDNSLookupHost(t *testing.T) {
|
||||
obj := NewDNSResolverImpl(time.Second*10, []string{dnsLoopbackAddr})
|
||||
|
||||
ip, _, err := obj.LookupHost("sigfail.verteiltesysteme.net")
|
||||
t.Logf("sigfail.verteiltesysteme.net - IP: %s, Err: %s", ip, err)
|
||||
test.AssertError(t, err, "DNSSEC failure")
|
||||
ip, _, _, err := obj.LookupHost("servfail.com")
|
||||
t.Logf("servfail.com - IP: %s, Err: %s", ip, err)
|
||||
test.AssertError(t, err, "Server failure")
|
||||
test.Assert(t, len(ip) == 0, "Should not have IPs")
|
||||
|
||||
ip, _, err = obj.LookupHost("nonexistent.letsencrypt.org")
|
||||
ip, _, _, err = obj.LookupHost("nonexistent.letsencrypt.org")
|
||||
t.Logf("nonexistent.letsencrypt.org - IP: %s, Err: %s", ip, err)
|
||||
test.AssertNotError(t, err, "Not an error to not exist")
|
||||
test.Assert(t, len(ip) == 0, "Should not have IPs")
|
||||
|
||||
ip, _, err = obj.LookupHost("cps.letsencrypt.org")
|
||||
ip, _, _, err = obj.LookupHost("cps.letsencrypt.org")
|
||||
t.Logf("cps.letsencrypt.org - IP: %s, Err: %s", ip, err)
|
||||
test.AssertNotError(t, err, "Not an error to be a CNAME")
|
||||
test.AssertNotError(t, err, "Not an error to exist")
|
||||
test.Assert(t, len(ip) > 0, "Should have IPs")
|
||||
}
|
||||
|
||||
func TestDNSLookupCAA(t *testing.T) {
|
||||
obj := NewDNSResolverImpl(time.Second*10, []string{dnsLoopbackAddr})
|
||||
|
||||
caas, err := obj.LookupCAA("bracewel.net", false)
|
||||
caas, _, err := obj.LookupCAA("bracewel.net")
|
||||
test.AssertNotError(t, err, "CAA lookup failed")
|
||||
test.Assert(t, len(caas) > 0, "Should have CAA records")
|
||||
|
||||
caas, err = obj.LookupCAA("nonexistent.letsencrypt.org", false)
|
||||
caas, _, err = obj.LookupCAA("nonexistent.letsencrypt.org")
|
||||
test.AssertNotError(t, err, "CAA lookup failed")
|
||||
test.Assert(t, len(caas) == 0, "Shouldn't have CAA records")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -140,10 +140,9 @@ type CertificateAuthorityDatabase interface {
|
|||
|
||||
// DNSResolver defines methods used for DNS resolution
|
||||
type DNSResolver interface {
|
||||
ExchangeOne(*dns.Msg) (*dns.Msg, time.Duration, error)
|
||||
LookupDNSSEC(*dns.Msg) (*dns.Msg, time.Duration, error)
|
||||
ExchangeOne(string, uint16) (*dns.Msg, time.Duration, error)
|
||||
LookupTXT(string) ([]string, time.Duration, error)
|
||||
LookupHost(string) ([]net.IP, time.Duration, error)
|
||||
LookupCNAME(string) (string, error)
|
||||
LookupCAA(string, bool) ([]*dns.CAA, error)
|
||||
LookupHost(string) ([]net.IP, time.Duration, time.Duration, error)
|
||||
LookupCNAME(string) (string, time.Duration, error)
|
||||
LookupCAA(string) ([]*dns.CAA, time.Duration, error)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,7 +65,6 @@ const (
|
|||
// Error types that can be used in ACME payloads
|
||||
const (
|
||||
ConnectionProblem = ProblemType("urn:acme:error:connection")
|
||||
DNSSECProblem = ProblemType("urn:acme:error:dnssec")
|
||||
MalformedProblem = ProblemType("urn:acme:error:malformed")
|
||||
ServerInternalProblem = ProblemType("urn:acme:error:serverInternal")
|
||||
TLSProblem = ProblemType("urn:acme:error:tls")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,136 @@
|
|||
#!/bin/bash
|
||||
# Copyright 2015 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/.
|
||||
#
|
||||
# This file creates individual AMQP accounts for each Boulder component,
|
||||
# and sets restrictive access controls on those accounts.
|
||||
#
|
||||
# You can use this tool without any configuration to produce users named
|
||||
# [am, ca, sa, ra, va, wfe, ocsp-updater] which all have the password "guest".
|
||||
# You can also customize this tool by creating a config file that will be
|
||||
# sourced. By default this file is obtained from $HOME/.rabbitmq_config, but
|
||||
# you can override the config file path using the environment variable
|
||||
# RABBITMQ_ACL_CONFIG, such as:
|
||||
#
|
||||
# $ RABBITMQ_ACL_CONFIG=myconfig ./rabbitmq_acl_configure.sh
|
||||
|
||||
# VARIABLES
|
||||
PORT=15672
|
||||
HOST=localhost
|
||||
VHOST="/"
|
||||
EXTRA=""
|
||||
RABBIT_ADMIN=$(which rabbitmqadmin)
|
||||
|
||||
# USER NAMES
|
||||
USER_BOULDER_AM="am"
|
||||
USER_BOULDER_CA="ca"
|
||||
USER_BOULDER_SA="sa"
|
||||
USER_BOULDER_RA="ra"
|
||||
USER_BOULDER_VA="va"
|
||||
USER_BOULDER_WFE="wfe"
|
||||
USER_BOULDER_OCSP="ocsp-updater"
|
||||
|
||||
# PASSWORDS
|
||||
PASS_BOULDER_AM="guest"
|
||||
PASS_BOULDER_CA="guest"
|
||||
PASS_BOULDER_SA="guest"
|
||||
PASS_BOULDER_RA="guest"
|
||||
PASS_BOULDER_VA="guest"
|
||||
PASS_BOULDER_WFE="guest"
|
||||
PASS_BOULDER_OCSP="guest"
|
||||
|
||||
# To use different options, you should create an override
|
||||
# file with whatever changes you want for the above variables
|
||||
RABBITMQ_ACL_CONFIG=${RABBITMQ_ACL_CONFIG:-$HOME/.rabbitmq_config}
|
||||
|
||||
if [ -r "${RABBITMQ_ACL_CONFIG}" ] ; then
|
||||
echo "Loading overrides from ${RABBITMQ_ACL_CONFIG}..."
|
||||
source "${RABBITMQ_ACL_CONFIG}"
|
||||
fi
|
||||
|
||||
if ! [ -x "${RABBIT_ADMIN}" ] ; then
|
||||
echo "Could not locate rabbitmqadmin; please set RABBIT_ADMIN in your ${RABBITMQ_ACL_CONFIG} file."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
run() {
|
||||
echo $*
|
||||
$*
|
||||
}
|
||||
|
||||
admin() {
|
||||
run ${RABBIT_ADMIN} -H ${HOST} -P ${PORT} -V ${VHOST} ${EXTRA} $*
|
||||
}
|
||||
|
||||
admin declare queue name="Monitor" durable=false
|
||||
admin declare queue name="CA.server" durable=false
|
||||
admin declare queue name="SA.server" durable=false
|
||||
admin declare queue name="RA.server" durable=false
|
||||
admin declare queue name="VA.server" durable=false
|
||||
|
||||
admin declare exchange name="boulder" type=topic durable=false
|
||||
|
||||
# Bind the wildcard topic (#) to Monitor, asking the server to copy all messages
|
||||
# and place them in the Montior queue.
|
||||
admin declare binding source="boulder" destination="Monitor" routing_key="#"
|
||||
|
||||
admin declare user name=${USER_BOULDER_AM} password=${PASS_BOULDER_AM} tags=""
|
||||
admin declare user name=${USER_BOULDER_CA} password=${PASS_BOULDER_CA} tags=""
|
||||
admin declare user name=${USER_BOULDER_SA} password=${PASS_BOULDER_SA} tags=""
|
||||
admin declare user name=${USER_BOULDER_RA} password=${PASS_BOULDER_RA} tags=""
|
||||
admin declare user name=${USER_BOULDER_VA} password=${PASS_BOULDER_VA} tags=""
|
||||
admin declare user name=${USER_BOULDER_WFE} password=${PASS_BOULDER_WFE} tags=""
|
||||
admin declare user name=${USER_BOULDER_OCSP} password=${PASS_BOULDER_OCSP} tags=""
|
||||
|
||||
##################################################
|
||||
## Permissions RegExes ##
|
||||
##################################################
|
||||
## Mystified? These are applied by the server ##
|
||||
## to various operations on queue names per ##
|
||||
## the decoder matrix here: ##
|
||||
## https://www.rabbitmq.com/access-control.html ##
|
||||
##################################################
|
||||
|
||||
# AM is read-only, and uses a predeclared Queue.
|
||||
admin declare permission vhost=${VHOST} user=${USER_BOULDER_AM} \
|
||||
configure="^$" \
|
||||
write="^$" \
|
||||
read="^Monitor$"
|
||||
|
||||
# VA uses VA.server, as well as dynamic queues named VA->RA.{hostname}.
|
||||
admin declare permission vhost=${VHOST} user=${USER_BOULDER_VA} \
|
||||
configure="^(VA\.server|VA->RA.*)$" \
|
||||
write="^(boulder|VA\.server|VA->RA.*)$" \
|
||||
read="^(boulder|VA\.server|VA->RA.*)$"
|
||||
|
||||
# RA uses RA.server, and RA->CA, RA->SA, RA->VA
|
||||
admin declare permission vhost=${VHOST} user=${USER_BOULDER_RA} \
|
||||
configure="^(RA\.server|RA->(CA|SA|VA).*)$" \
|
||||
write="^(boulder|RA\.server|RA->(CA|SA|VA).*)$" \
|
||||
read="^(boulder|RA\.server|RA->(CA|SA|VA).*)$"
|
||||
|
||||
# CA uses CA.server, and CA->SA
|
||||
admin declare permission vhost=${VHOST} user=${USER_BOULDER_CA} \
|
||||
configure="^(CA\.server|CA->SA.*)$" \
|
||||
write="^(boulder|CA\.server|CA->SA.*)$" \
|
||||
read="^(boulder|CA\.server|CA->SA.*)$"
|
||||
|
||||
# SA uses only SA.server
|
||||
admin declare permission vhost=${VHOST} user=${USER_BOULDER_SA} \
|
||||
configure="^SA\.server$" \
|
||||
write="^(boulder|SA\.server)$" \
|
||||
read="^(boulder|SA\.server)$"
|
||||
|
||||
# WFE uses WFE->RA and WFE->SA
|
||||
admin declare permission vhost=${VHOST} user=${USER_BOULDER_WFE} \
|
||||
configure="^(WFE->(RA|SA).*)$" \
|
||||
write="^(boulder|WFE->(RA|SA).*)$" \
|
||||
read="^(boulder|WFE->(RA|SA).*)$"
|
||||
|
||||
# OCSP uses only OCSP->CA
|
||||
admin declare permission vhost=${VHOST} user=${USER_BOULDER_OCSP} \
|
||||
configure="^(OCSP->CA.*)$" \
|
||||
write="^(boulder|OCSP->CA.*)$" \
|
||||
read="^(boulder|OCSP->CA.*)$"
|
||||
|
|
@ -19,6 +19,21 @@ import (
|
|||
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
|
||||
)
|
||||
|
||||
// A SyslogWriter logs messages with explicit priority levels. It is
|
||||
// implemented by a logging back-end like syslog.Writer or
|
||||
// mocks.SyslogWriter.
|
||||
type SyslogWriter interface {
|
||||
Close() error
|
||||
Alert(m string) error
|
||||
Crit(m string) error
|
||||
Debug(m string) error
|
||||
Emerg(m string) error
|
||||
Err(m string) error
|
||||
Info(m string) error
|
||||
Notice(m string) error
|
||||
Warning(m string) error
|
||||
}
|
||||
|
||||
// singleton defines the object of a Singleton pattern
|
||||
type singleton struct {
|
||||
once sync.Once
|
||||
|
|
@ -42,13 +57,11 @@ func defaultEmergencyExit() {
|
|||
os.Exit(emergencyReturnValue)
|
||||
}
|
||||
|
||||
// AuditLogger is a System Logger with additional audit-specific methods.
|
||||
// In addition to all the standard syslog.Writer methods from
|
||||
// http://golang.org/pkg/log/syslog/#Writer, you can also call
|
||||
// auditLogger.Audit(msg string)
|
||||
// to send a message as an audit event.
|
||||
// AuditLogger implements SyslogWriter, and has additional
|
||||
// audit-specific methods, like Audit(), for indicating which messages
|
||||
// should be classified as audit events.
|
||||
type AuditLogger struct {
|
||||
*syslog.Writer
|
||||
SyslogWriter
|
||||
Stats statsd.Statter
|
||||
exitFunction exitFunction
|
||||
}
|
||||
|
|
@ -64,9 +77,9 @@ func Dial(network, raddr string, tag string, stats statsd.Statter) (*AuditLogger
|
|||
return NewAuditLogger(syslogger, stats)
|
||||
}
|
||||
|
||||
// NewAuditLogger constructs an Audit Logger that decorates a normal
|
||||
// System Logger. All methods in log/syslog continue to work.
|
||||
func NewAuditLogger(log *syslog.Writer, stats statsd.Statter) (*AuditLogger, error) {
|
||||
// NewAuditLogger returns a new AuditLogger that uses the given
|
||||
// SyslogWriter as a backend.
|
||||
func NewAuditLogger(log SyslogWriter, stats statsd.Statter) (*AuditLogger, error) {
|
||||
if log == nil {
|
||||
return nil, errors.New("Attempted to use a nil System Logger.")
|
||||
}
|
||||
|
|
@ -125,21 +138,21 @@ func (log *AuditLogger) logAtLevel(level, msg string) (err error) {
|
|||
|
||||
switch level {
|
||||
case "Logging.Alert":
|
||||
err = log.Writer.Alert(msg)
|
||||
err = log.SyslogWriter.Alert(msg)
|
||||
case "Logging.Crit":
|
||||
err = log.Writer.Crit(msg)
|
||||
err = log.SyslogWriter.Crit(msg)
|
||||
case "Logging.Debug":
|
||||
err = log.Writer.Debug(msg)
|
||||
err = log.SyslogWriter.Debug(msg)
|
||||
case "Logging.Emerg":
|
||||
err = log.Writer.Emerg(msg)
|
||||
err = log.SyslogWriter.Emerg(msg)
|
||||
case "Logging.Err":
|
||||
err = log.Writer.Err(msg)
|
||||
err = log.SyslogWriter.Err(msg)
|
||||
case "Logging.Info":
|
||||
err = log.Writer.Info(msg)
|
||||
err = log.SyslogWriter.Info(msg)
|
||||
case "Logging.Warning":
|
||||
err = log.Writer.Warning(msg)
|
||||
err = log.SyslogWriter.Warning(msg)
|
||||
case "Logging.Notice":
|
||||
err = log.Writer.Notice(msg)
|
||||
err = log.SyslogWriter.Notice(msg)
|
||||
default:
|
||||
err = fmt.Errorf("Unknown logging level: %s", level)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,172 @@
|
|||
// Copyright 2015 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 mocks
|
||||
|
||||
import (
|
||||
"log"
|
||||
"log/syslog"
|
||||
"regexp"
|
||||
|
||||
blog "github.com/letsencrypt/boulder/log"
|
||||
)
|
||||
|
||||
// MockSyslogWriter implements the blog.SyslogWriter interface. It
|
||||
// stores all logged messages in a buffer for inspection by test
|
||||
// functions (via GetAll()) instead of sending them to syslog.
|
||||
type MockSyslogWriter struct {
|
||||
logged []*LogMessage
|
||||
msgChan chan<- *LogMessage
|
||||
getChan <-chan []*LogMessage
|
||||
clearChan chan<- struct{}
|
||||
closeChan chan<- struct{}
|
||||
}
|
||||
|
||||
// LogMessage is a log entry that has been sent to a MockSyslogWriter.
|
||||
type LogMessage struct {
|
||||
Priority syslog.Priority // aka Log level
|
||||
Message string // content of log message
|
||||
}
|
||||
|
||||
var levelName = map[syslog.Priority]string{
|
||||
syslog.LOG_EMERG: "EMERG",
|
||||
syslog.LOG_ALERT: "ALERT",
|
||||
syslog.LOG_CRIT: "CRIT",
|
||||
syslog.LOG_ERR: "ERR",
|
||||
syslog.LOG_WARNING: "WARNING",
|
||||
syslog.LOG_NOTICE: "NOTICE",
|
||||
syslog.LOG_INFO: "INFO",
|
||||
syslog.LOG_DEBUG: "DEBUG",
|
||||
}
|
||||
|
||||
func (lm *LogMessage) String() string {
|
||||
return levelName[lm.Priority&7] + ": " + lm.Message
|
||||
}
|
||||
|
||||
// UseMockLog changes the SyslogWriter used by the current singleton
|
||||
// audit logger to a new mock logger, and returns the mock. Example:
|
||||
//
|
||||
// var log = mocks.UseMockLog()
|
||||
// func TestFoo(t *testing.T) {
|
||||
// log.Clear()
|
||||
// // ...
|
||||
// Assert(t, len(log.GetAll()) > 0, "Should have logged something")
|
||||
// }
|
||||
func UseMockLog() *MockSyslogWriter {
|
||||
sw := NewSyslogWriter()
|
||||
blog.GetAuditLogger().SyslogWriter = sw
|
||||
return sw
|
||||
}
|
||||
|
||||
// NewSyslogWriter returns a new MockSyslogWriter.
|
||||
func NewSyslogWriter() *MockSyslogWriter {
|
||||
msgChan := make(chan *LogMessage)
|
||||
getChan := make(chan []*LogMessage)
|
||||
clearChan := make(chan struct{})
|
||||
closeChan := make(chan struct{})
|
||||
msw := &MockSyslogWriter{
|
||||
logged: []*LogMessage{},
|
||||
msgChan: msgChan,
|
||||
getChan: getChan,
|
||||
clearChan: clearChan,
|
||||
closeChan: closeChan,
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case logMsg := <-msgChan:
|
||||
log.Print("MockSyslog:" + logMsg.String())
|
||||
msw.logged = append(msw.logged, logMsg)
|
||||
case getChan <- msw.logged:
|
||||
case <-clearChan:
|
||||
msw.logged = []*LogMessage{}
|
||||
case <-closeChan:
|
||||
close(getChan)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
return msw
|
||||
}
|
||||
|
||||
func (msw *MockSyslogWriter) write(m string, priority syslog.Priority) error {
|
||||
msw.msgChan <- &LogMessage{Message: m, Priority: priority}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAll returns all LogMessages logged (since the last call to
|
||||
// Clear(), if applicable).
|
||||
//
|
||||
// The caller must not modify the returned slice or its elements.
|
||||
func (msw *MockSyslogWriter) GetAll() []*LogMessage {
|
||||
return <-msw.getChan
|
||||
}
|
||||
|
||||
// GetAllMatching returns all LogMessages logged (since the last
|
||||
// Clear()) whose text matches the given regexp. The regexp is
|
||||
// accepted as a string and compiled on the fly, because convenience
|
||||
// is more important than performance.
|
||||
//
|
||||
// The caller must not modify the elements of the returned slice.
|
||||
func (msw *MockSyslogWriter) GetAllMatching(reString string) (matches []*LogMessage) {
|
||||
re := regexp.MustCompile(reString)
|
||||
for _, logMsg := range <-msw.getChan {
|
||||
if re.MatchString(logMsg.Message) {
|
||||
matches = append(matches, logMsg)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Clear resets the log buffer.
|
||||
func (msw *MockSyslogWriter) Clear() {
|
||||
msw.clearChan <- struct{}{}
|
||||
}
|
||||
|
||||
// Close releases resources. No other methods may be called after this.
|
||||
func (msw *MockSyslogWriter) Close() error {
|
||||
msw.closeChan <- struct{}{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Alert logs at LOG_ALERT
|
||||
func (msw *MockSyslogWriter) Alert(m string) error {
|
||||
return msw.write(m, syslog.LOG_ALERT)
|
||||
}
|
||||
|
||||
// Crit logs at LOG_CRIT
|
||||
func (msw *MockSyslogWriter) Crit(m string) error {
|
||||
return msw.write(m, syslog.LOG_CRIT)
|
||||
}
|
||||
|
||||
// Debug logs at LOG_DEBUG
|
||||
func (msw *MockSyslogWriter) Debug(m string) error {
|
||||
return msw.write(m, syslog.LOG_DEBUG)
|
||||
}
|
||||
|
||||
// Emerg logs at LOG_EMERG
|
||||
func (msw *MockSyslogWriter) Emerg(m string) error {
|
||||
return msw.write(m, syslog.LOG_EMERG)
|
||||
}
|
||||
|
||||
// Err logs at LOG_ERR
|
||||
func (msw *MockSyslogWriter) Err(m string) error {
|
||||
return msw.write(m, syslog.LOG_ERR)
|
||||
}
|
||||
|
||||
// Info logs at LOG_INFO
|
||||
func (msw *MockSyslogWriter) Info(m string) error {
|
||||
return msw.write(m, syslog.LOG_INFO)
|
||||
}
|
||||
|
||||
// Notice logs at LOG_NOTICE
|
||||
func (msw *MockSyslogWriter) Notice(m string) error {
|
||||
return msw.write(m, syslog.LOG_NOTICE)
|
||||
}
|
||||
|
||||
// Warning logs at LOG_WARNING
|
||||
func (msw *MockSyslogWriter) Warning(m string) error {
|
||||
return msw.write(m, syslog.LOG_WARNING)
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ package mocks
|
|||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
|
|
@ -14,7 +15,6 @@ import (
|
|||
_ "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/mattn/go-sqlite3"
|
||||
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/miekg/dns"
|
||||
gorp "github.com/letsencrypt/boulder/Godeps/_workspace/src/gopkg.in/gorp.v1"
|
||||
"github.com/letsencrypt/boulder/core"
|
||||
)
|
||||
|
||||
// MockCADatabase is a mock
|
||||
|
|
@ -52,35 +52,30 @@ type MockDNS struct {
|
|||
}
|
||||
|
||||
// ExchangeOne is a mock
|
||||
func (mock *MockDNS) ExchangeOne(m *dns.Msg) (rsp *dns.Msg, rtt time.Duration, err error) {
|
||||
return m, 0, nil
|
||||
func (mock *MockDNS) ExchangeOne(hostname string, qt uint16) (rsp *dns.Msg, rtt time.Duration, err error) {
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
||||
// LookupTXT is a mock
|
||||
func (mock *MockDNS) LookupTXT(hostname string) ([]string, time.Duration, error) {
|
||||
if hostname == "_acme-challenge.dnssec-failed.org" {
|
||||
return nil, 0, core.DNSSECError{}
|
||||
if hostname == "_acme-challenge.servfail.com" {
|
||||
return nil, 0, fmt.Errorf("SERVFAIL")
|
||||
}
|
||||
return []string{"hostname"}, 0, nil
|
||||
}
|
||||
|
||||
// LookupDNSSEC is a mock
|
||||
func (mock *MockDNS) LookupDNSSEC(m *dns.Msg) (*dns.Msg, time.Duration, error) {
|
||||
return m, 0, nil
|
||||
}
|
||||
|
||||
// LookupHost is a mock
|
||||
func (mock *MockDNS) LookupHost(hostname string) ([]net.IP, time.Duration, error) {
|
||||
return nil, 0, nil
|
||||
func (mock *MockDNS) LookupHost(hostname string) ([]net.IP, time.Duration, time.Duration, error) {
|
||||
return nil, 0, 0, nil
|
||||
}
|
||||
|
||||
// LookupCNAME is a mock
|
||||
func (mock *MockDNS) LookupCNAME(domain string) (string, error) {
|
||||
return "hostname", nil
|
||||
func (mock *MockDNS) LookupCNAME(domain string) (string, time.Duration, error) {
|
||||
return "hostname", 0, nil
|
||||
}
|
||||
|
||||
// LookupCAA is a mock
|
||||
func (mock *MockDNS) LookupCAA(domain string, alias bool) ([]*dns.CAA, error) {
|
||||
func (mock *MockDNS) LookupCAA(domain string) ([]*dns.CAA, time.Duration, error) {
|
||||
var results []*dns.CAA
|
||||
var record dns.CAA
|
||||
switch domain {
|
||||
|
|
@ -97,8 +92,8 @@ func (mock *MockDNS) LookupCAA(domain string, alias bool) ([]*dns.CAA, error) {
|
|||
record.Tag = "issue"
|
||||
record.Value = "letsencrypt.org"
|
||||
results = append(results, &record)
|
||||
case "dnssec-failed.org":
|
||||
return results, core.DNSSECError{}
|
||||
case "servfail.com":
|
||||
return results, 0, fmt.Errorf("SERVFAIL")
|
||||
}
|
||||
return results, nil
|
||||
return results, 0, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,11 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/letsencrypt/boulder/core"
|
||||
"github.com/letsencrypt/boulder/mocks"
|
||||
)
|
||||
|
||||
var log = mocks.UseMockLog()
|
||||
|
||||
func TestWillingToIssue(t *testing.T) {
|
||||
shouldBeSyntaxError := []string{
|
||||
``, // Empty name
|
||||
|
|
|
|||
|
|
@ -132,6 +132,8 @@ var (
|
|||
}
|
||||
AuthzUpdated = core.Authorization{}
|
||||
AuthzFinal = core.Authorization{}
|
||||
|
||||
log = mocks.UseMockLog()
|
||||
)
|
||||
|
||||
func initAuthorities(t *testing.T) (core.CertificateAuthority, *DummyValidationAuthority, *sa.SQLStorageAuthority, core.RegistrationAuthority) {
|
||||
|
|
|
|||
|
|
@ -43,20 +43,21 @@ const (
|
|||
AmqpImmediate = false
|
||||
)
|
||||
|
||||
// A simplified way to get a channel for a given AMQP server
|
||||
func amqpConnect(url string) (ch *amqp.Channel, err error) {
|
||||
conn, err := amqp.Dial(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// AMQPDeclareExchange attempts to declare the configured AMQP exchange,
|
||||
// returning silently if already declared, erroring if nonexistant and
|
||||
// unable to create.
|
||||
func AMQPDeclareExchange(conn *amqp.Connection) error {
|
||||
var err error
|
||||
var ch *amqp.Channel
|
||||
log := blog.GetAuditLogger()
|
||||
|
||||
ch, err = conn.Channel()
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Crit(fmt.Sprintf("Could not connect Channel: %s", err))
|
||||
return err
|
||||
}
|
||||
|
||||
// A simplified way to declare and subscribe to an AMQP queue
|
||||
func amqpSubscribe(ch *amqp.Channel, name string, log *blog.AuditLogger) (msgs <-chan amqp.Delivery, err error) {
|
||||
err = ch.ExchangeDeclare(
|
||||
err = ch.ExchangeDeclarePassive(
|
||||
AmqpExchange,
|
||||
AmqpExchangeType,
|
||||
AmqpDurable,
|
||||
|
|
@ -65,11 +66,40 @@ func amqpSubscribe(ch *amqp.Channel, name string, log *blog.AuditLogger) (msgs <
|
|||
AmqpNoWait,
|
||||
nil)
|
||||
if err != nil {
|
||||
log.Crit(fmt.Sprintf("Could not declare exchange: %s", err))
|
||||
return
|
||||
log.Info(fmt.Sprintf("Exchange %s does not exist on AMQP server, attempting to create. (err=%s)", AmqpExchange, err))
|
||||
|
||||
// Channel is invalid at this point, so recreate
|
||||
ch.Close()
|
||||
ch, err = conn.Channel()
|
||||
if err != nil {
|
||||
log.Crit(fmt.Sprintf("Could not connect Channel: %s", err))
|
||||
return err
|
||||
}
|
||||
|
||||
err = ch.ExchangeDeclare(
|
||||
AmqpExchange,
|
||||
AmqpExchangeType,
|
||||
AmqpDurable,
|
||||
AmqpDeleteUnused,
|
||||
AmqpInternal,
|
||||
AmqpNoWait,
|
||||
nil)
|
||||
if err != nil {
|
||||
log.Crit(fmt.Sprintf("Could not declare exchange: %s", err))
|
||||
ch.Close()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
q, err := ch.QueueDeclare(
|
||||
ch.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
// A simplified way to declare and subscribe to an AMQP queue
|
||||
func amqpSubscribe(ch *amqp.Channel, name string, log *blog.AuditLogger) (<-chan amqp.Delivery, error) {
|
||||
var err error
|
||||
|
||||
_, err = ch.QueueDeclare(
|
||||
name,
|
||||
AmqpDurable,
|
||||
AmqpDeleteUnused,
|
||||
|
|
@ -78,22 +108,24 @@ func amqpSubscribe(ch *amqp.Channel, name string, log *blog.AuditLogger) (msgs <
|
|||
nil)
|
||||
if err != nil {
|
||||
log.Crit(fmt.Sprintf("Could not declare queue: %s", err))
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
routingKey := name
|
||||
|
||||
err = ch.QueueBind(
|
||||
name,
|
||||
name,
|
||||
routingKey,
|
||||
AmqpExchange,
|
||||
false,
|
||||
nil)
|
||||
if err != nil {
|
||||
log.Crit(fmt.Sprintf("Could not bind queue: %s", err))
|
||||
return
|
||||
log.Crit(fmt.Sprintf("Could not bind to queue [%s]. NOTE: You may need to delete %s to re-trigger the bind attempt after fixing permissions, or manually bind the queue to %s.", name, name, routingKey))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msgs, err = ch.Consume(
|
||||
q.Name,
|
||||
msgs, err := ch.Consume(
|
||||
name,
|
||||
"",
|
||||
AmqpAutoAck,
|
||||
AmqpExclusive,
|
||||
|
|
@ -102,10 +134,10 @@ func amqpSubscribe(ch *amqp.Channel, name string, log *blog.AuditLogger) (msgs <
|
|||
nil)
|
||||
if err != nil {
|
||||
log.Crit(fmt.Sprintf("Could not subscribe to queue: %s", err))
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return
|
||||
return msgs, err
|
||||
}
|
||||
|
||||
// AmqpRPCServer listens on a specified queue within an AMQP channel.
|
||||
|
|
|
|||
|
|
@ -13,9 +13,12 @@ import (
|
|||
jose "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/square/go-jose"
|
||||
|
||||
"github.com/letsencrypt/boulder/core"
|
||||
"github.com/letsencrypt/boulder/mocks"
|
||||
"github.com/letsencrypt/boulder/test"
|
||||
)
|
||||
|
||||
var log = mocks.UseMockLog()
|
||||
|
||||
const JWK1JSON = `{
|
||||
"kty": "RSA",
|
||||
"n": "vuc785P8lBj3fUxyZchF_uZw6WtbxcorqgTyq-qapF5lrO1U82Tp93rpXlmctj6fyFHBVVB5aXnUHJ7LZeVPod7Wnfl8p5OyhlHQHC8BnzdzCqCMKmWZNX5DtETDId0qzU7dPzh0LP0idt5buU7L9QNaabChw3nnaL47iu_1Di5Wp264p2TwACeedv2hfRDjDlJmaQXuS8Rtv9GnRWyC9JBu7XmGvGDziumnJH7Hyzh3VNu-kSPQD3vuAFgMZS6uUzOztCkT0fpOalZI6hqxtWLvXUMj-crXrn-Maavz8qRhpAyp5kcYk3jiHGgQIi7QSK2JIdRJ8APyX9HlmTN5AQ",
|
||||
|
|
|
|||
|
|
@ -18,11 +18,14 @@ import (
|
|||
_ "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/mattn/go-sqlite3"
|
||||
jose "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/square/go-jose"
|
||||
"github.com/letsencrypt/boulder/core"
|
||||
"github.com/letsencrypt/boulder/mocks"
|
||||
"github.com/letsencrypt/boulder/test"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var log = mocks.UseMockLog()
|
||||
|
||||
func initSA(t *testing.T) *SQLStorageAuthority {
|
||||
sa, err := NewSQLStorageAuthority("sqlite3", ":memory:")
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -33,25 +33,26 @@ class ProcInfo:
|
|||
self.cmd = cmd
|
||||
self.proc = proc
|
||||
|
||||
def run(path):
|
||||
def run(path, args=""):
|
||||
global processes
|
||||
binary = os.path.join(tempdir, os.path.basename(path))
|
||||
cmd = 'GORACE="halt_on_error=1" go build -tags pkcs11 -race -o %s %s' % (binary, path)
|
||||
print(cmd)
|
||||
if subprocess.Popen(cmd, shell=True).wait() != 0:
|
||||
die(ExitStatus.Error)
|
||||
runCmd = "exec %s --config test/boulder-test-config.json" % binary
|
||||
runCmd = "exec %s %s" % (binary, args)
|
||||
print(runCmd)
|
||||
info = ProcInfo(runCmd, subprocess.Popen(runCmd, shell=True))
|
||||
processes.append(info)
|
||||
return info
|
||||
|
||||
def start():
|
||||
run('./cmd/boulder-wfe')
|
||||
run('./cmd/boulder-ra')
|
||||
run('./cmd/boulder-sa')
|
||||
run('./cmd/boulder-ca')
|
||||
run('./cmd/boulder-va')
|
||||
run('./cmd/boulder-wfe', '--config test/boulder-test-config.json')
|
||||
run('./cmd/boulder-ra', '--config test/boulder-test-config.json')
|
||||
run('./cmd/boulder-sa', '--config test/boulder-test-config.json')
|
||||
run('./cmd/boulder-ca', '--config test/boulder-test-config.json')
|
||||
run('./cmd/boulder-va', '--config test/boulder-test-config.json')
|
||||
run('./test/dns-test-srv')
|
||||
|
||||
def run_node_test():
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
|
|
|
|||
|
|
@ -105,6 +105,10 @@
|
|||
}
|
||||
},
|
||||
|
||||
"monolith": {
|
||||
"debugAddr": "localhost:8008"
|
||||
},
|
||||
|
||||
"ra": {
|
||||
"debugAddr": "localhost:8002"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@
|
|||
},
|
||||
|
||||
"va": {
|
||||
"dnsResolver": "8.8.8.8:53",
|
||||
"dnsResolver": "127.0.0.1:8053",
|
||||
"dnsTimeout": "10s",
|
||||
"userAgent": "boulder",
|
||||
"debugAddr": "localhost:8004"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
// Copyright 2015 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 main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func dnsHandler(w dns.ResponseWriter, r *dns.Msg) {
|
||||
defer w.Close()
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
m.Compress = false
|
||||
|
||||
for _, q := range r.Question {
|
||||
fmt.Printf("dns-srv: Query -- [%s] %s\n", q.Name, dns.TypeToString[q.Qtype])
|
||||
switch q.Qtype {
|
||||
case dns.TypeA:
|
||||
record := new(dns.A)
|
||||
record.Hdr = dns.RR_Header{
|
||||
Name: q.Name,
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 0,
|
||||
}
|
||||
record.A = net.ParseIP("127.0.0.1")
|
||||
|
||||
m.Answer = append(m.Answer, record)
|
||||
case dns.TypeMX:
|
||||
record := new(dns.MX)
|
||||
record.Hdr = dns.RR_Header{
|
||||
Name: q.Name,
|
||||
Rrtype: dns.TypeMX,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 0,
|
||||
}
|
||||
record.Mx = "mail." + q.Name
|
||||
record.Preference = 10
|
||||
|
||||
m.Answer = append(m.Answer, record)
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteMsg(m)
|
||||
return
|
||||
}
|
||||
|
||||
func serveTestResolver() {
|
||||
dns.HandleFunc(".", dnsHandler)
|
||||
server := &dns.Server{
|
||||
Addr: "127.0.0.1:8053",
|
||||
Net: "udp",
|
||||
ReadTimeout: time.Millisecond,
|
||||
WriteTimeout: time.Millisecond,
|
||||
}
|
||||
go func() {
|
||||
err := server.ListenAndServe()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func main() {
|
||||
fmt.Println("dns-srv: Starting test DNS server")
|
||||
serveTestResolver()
|
||||
forever := make(chan bool, 1)
|
||||
<-forever
|
||||
}
|
||||
|
|
@ -54,7 +54,7 @@ func AssertError(t *testing.T, err error, message string) {
|
|||
}
|
||||
}
|
||||
|
||||
// AssertEquals uses the equality operator (=) to measure one and two
|
||||
// AssertEquals uses the equality operator (==) to measure one and two
|
||||
func AssertEquals(t *testing.T, one interface{}, two interface{}) {
|
||||
if one != two {
|
||||
t.Errorf("%s [%v] != [%v]", caller(), one, two)
|
||||
|
|
|
|||
|
|
@ -54,6 +54,23 @@ type verificationRequestEvent struct {
|
|||
|
||||
// Validation methods
|
||||
|
||||
// setChallengeErrorFromDNSError checks the error returned from Lookup...
|
||||
// methods and tests if the error was an underlying net.OpError or an error
|
||||
// caused by resolver returning SERVFAIL or other invalid Rcodes and sets
|
||||
// the challenge.Error field accordingly.
|
||||
func setChallengeErrorFromDNSError(err error, challenge *core.Challenge) {
|
||||
challenge.Error = &core.ProblemDetails{Type: core.ConnectionProblem}
|
||||
if netErr, ok := err.(*net.OpError); ok {
|
||||
if netErr.Timeout() {
|
||||
challenge.Error.Detail = "DNS query timed out"
|
||||
} else if netErr.Temporary() {
|
||||
challenge.Error.Detail = "Temporary network connectivity error"
|
||||
}
|
||||
} else {
|
||||
challenge.Error.Detail = "Server failure at resolver"
|
||||
}
|
||||
}
|
||||
|
||||
func (va ValidationAuthorityImpl) validateSimpleHTTP(identifier core.AcmeIdentifier, input core.Challenge) (core.Challenge, error) {
|
||||
challenge := input
|
||||
|
||||
|
|
@ -301,19 +318,9 @@ func (va ValidationAuthorityImpl) validateDNS(identifier core.AcmeIdentifier, in
|
|||
txts, _, err := va.DNSResolver.LookupTXT(challengeSubdomain)
|
||||
|
||||
if err != nil {
|
||||
if dnssecErr, ok := err.(core.DNSSECError); ok {
|
||||
challenge.Error = &core.ProblemDetails{
|
||||
Type: core.DNSSECProblem,
|
||||
Detail: dnssecErr.Error(),
|
||||
}
|
||||
} else {
|
||||
challenge.Error = &core.ProblemDetails{
|
||||
Type: core.ServerInternalProblem,
|
||||
Detail: "Unable to communicate with DNS server",
|
||||
}
|
||||
}
|
||||
challenge.Status = core.StatusInvalid
|
||||
va.log.Debug(fmt.Sprintf("DNS [%s] DNS failure: %s", identifier, err))
|
||||
setChallengeErrorFromDNSError(err, &challenge)
|
||||
va.log.Debug(fmt.Sprintf("%s [%s] DNS failure: %s", challenge.Type, identifier, err))
|
||||
return challenge, challenge.Error
|
||||
}
|
||||
|
||||
|
|
@ -429,9 +436,9 @@ func newCAASet(CAAs []*dns.CAA) *CAASet {
|
|||
return &filtered
|
||||
}
|
||||
|
||||
func (va *ValidationAuthorityImpl) getCAASet(domain string, dnsResolver core.DNSResolver) (*CAASet, error) {
|
||||
domain = strings.TrimRight(domain, ".")
|
||||
splitDomain := strings.Split(domain, ".")
|
||||
func (va *ValidationAuthorityImpl) getCAASet(hostname string) (*CAASet, error) {
|
||||
hostname = strings.TrimRight(hostname, ".")
|
||||
splitDomain := strings.Split(hostname, ".")
|
||||
// RFC 6844 CAA set query sequence, 'x.y.z.com' => ['x.y.z.com', 'y.z.com', 'z.com']
|
||||
for i := range splitDomain {
|
||||
queryDomain := strings.Join(splitDomain[i:], ".")
|
||||
|
|
@ -442,7 +449,14 @@ func (va *ValidationAuthorityImpl) getCAASet(domain string, dnsResolver core.DNS
|
|||
|
||||
// Query CAA records for domain and its alias if it has a CNAME
|
||||
for _, alias := range []bool{false, true} {
|
||||
CAAs, err := va.DNSResolver.LookupCAA(queryDomain, alias)
|
||||
if alias {
|
||||
target, _, err := va.DNSResolver.LookupCNAME(queryDomain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
queryDomain = target
|
||||
}
|
||||
CAAs, _, err := va.DNSResolver.LookupCAA(queryDomain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -460,8 +474,8 @@ func (va *ValidationAuthorityImpl) getCAASet(domain string, dnsResolver core.DNS
|
|||
// CheckCAARecords verifies that, if the indicated subscriber domain has any CAA
|
||||
// records, they authorize the configured CA domain to issue a certificate
|
||||
func (va *ValidationAuthorityImpl) CheckCAARecords(identifier core.AcmeIdentifier) (present, valid bool, err error) {
|
||||
domain := strings.ToLower(identifier.Value)
|
||||
caaSet, err := va.getCAASet(domain, va.DNSResolver)
|
||||
hostname := strings.ToLower(identifier.Value)
|
||||
caaSet, err := va.getCAASet(hostname)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -477,7 +491,7 @@ func (va *ValidationAuthorityImpl) CheckCAARecords(identifier core.AcmeIdentifie
|
|||
} else if len(caaSet.Issue) > 0 || len(caaSet.Issuewild) > 0 {
|
||||
present = true
|
||||
var checkSet []*dns.CAA
|
||||
if strings.SplitN(domain, ".", 2)[0] == "*" {
|
||||
if strings.SplitN(hostname, ".", 2)[0] == "*" {
|
||||
checkSet = caaSet.Issuewild
|
||||
} else {
|
||||
checkSet = caaSet.Issue
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import (
|
|||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log/syslog"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
|
|
@ -51,15 +52,15 @@ var TheKey = rsa.PrivateKey{
|
|||
|
||||
var ident = core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "localhost"}
|
||||
|
||||
const (
|
||||
expectedToken = "THETOKEN"
|
||||
pathWrongToken = "wrongtoken"
|
||||
path404 = "404"
|
||||
pathFound = "302"
|
||||
pathMoved = "301"
|
||||
pathUnsafe = "%"
|
||||
pathUnsafe302 = "302-to-unsafe"
|
||||
)
|
||||
var log = mocks.UseMockLog()
|
||||
|
||||
const expectedToken = "THETOKEN"
|
||||
const pathWrongToken = "wrongtoken"
|
||||
const path404 = "404"
|
||||
const pathFound = "302"
|
||||
const pathMoved = "301"
|
||||
const pathUnsafe = "%"
|
||||
const pathUnsafe302 = "302-to-unsafe"
|
||||
|
||||
func simpleSrv(t *testing.T, token string, stopChan, waitChan chan bool, enableTLS bool) {
|
||||
m := http.NewServeMux()
|
||||
|
|
@ -225,9 +226,13 @@ func TestSimpleHttpTLS(t *testing.T) {
|
|||
defer func() { stopChan <- true }()
|
||||
<-waitChan
|
||||
|
||||
log.Clear()
|
||||
finChall, err := va.validateSimpleHTTP(ident, chall)
|
||||
test.AssertEquals(t, finChall.Status, core.StatusValid)
|
||||
test.AssertNotError(t, err, chall.Path)
|
||||
logs := log.GetAllMatching(`^\[AUDIT\] Attempting to validate SimpleHTTPS for `)
|
||||
test.AssertEquals(t, len(logs), 1)
|
||||
test.AssertEquals(t, logs[0].Priority, syslog.LOG_NOTICE)
|
||||
}
|
||||
|
||||
func TestSimpleHttpRedirect(t *testing.T) {
|
||||
|
|
@ -260,27 +265,33 @@ func TestSimpleHttp(t *testing.T) {
|
|||
defer func() { stopChan <- true }()
|
||||
<-waitChan
|
||||
|
||||
log.Clear()
|
||||
finChall, err := va.validateSimpleHTTP(ident, chall)
|
||||
test.AssertEquals(t, finChall.Status, core.StatusValid)
|
||||
test.AssertNotError(t, err, chall.Path)
|
||||
test.AssertEquals(t, len(log.GetAllMatching(`^\[AUDIT\] `)), 1)
|
||||
|
||||
log.Clear()
|
||||
chall.Path = pathMoved
|
||||
finChall, err = va.validateSimpleHTTP(ident, chall)
|
||||
test.AssertEquals(t, finChall.Status, core.StatusValid)
|
||||
test.AssertNotError(t, err, chall.Path)
|
||||
// TODO: verify the redirect was logged.
|
||||
|
||||
log.Clear()
|
||||
chall.Path = pathFound
|
||||
finChall, err = va.validateSimpleHTTP(ident, chall)
|
||||
test.AssertEquals(t, finChall.Status, core.StatusValid)
|
||||
test.AssertNotError(t, err, chall.Path)
|
||||
// TODO: verify two redirects were logged.
|
||||
|
||||
log.Clear()
|
||||
chall.Path = path404
|
||||
invalidChall, err = va.validateSimpleHTTP(ident, chall)
|
||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
test.AssertError(t, err, "Should have found a 404 for the challenge.")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.UnauthorizedProblem)
|
||||
test.AssertEquals(t, len(log.GetAllMatching(`^\[AUDIT\] `)), 1)
|
||||
|
||||
chall.Path = pathWrongToken
|
||||
invalidChall, err = va.validateSimpleHTTP(ident, chall)
|
||||
|
|
@ -365,8 +376,8 @@ func TestDvsni(t *testing.T) {
|
|||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
test.AssertError(t, err, "Domain name is invalid.")
|
||||
test.AssertEquals(t, invalidChall.Error.Type, core.UnknownHostProblem)
|
||||
va.TestMode = true
|
||||
|
||||
va.TestMode = true
|
||||
chall.R = ba[5:]
|
||||
invalidChall, err = va.validateDvsni(ident, chall)
|
||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
||||
|
|
@ -581,8 +592,8 @@ func TestCAAChecking(t *testing.T) {
|
|||
test.AssertEquals(t, caaTest.Valid, valid)
|
||||
}
|
||||
|
||||
present, valid, err := va.CheckCAARecords(core.AcmeIdentifier{Type: "dns", Value: "dnssec-failed.org"})
|
||||
test.AssertError(t, err, "dnssec-failed.org")
|
||||
present, valid, err := va.CheckCAARecords(core.AcmeIdentifier{Type: "dns", Value: "servfail.com"})
|
||||
test.AssertError(t, err, "servfail.com")
|
||||
test.Assert(t, !present, "Present should be false")
|
||||
test.Assert(t, !valid, "Valid should be false")
|
||||
}
|
||||
|
|
@ -675,7 +686,7 @@ func TestDNSValidationNotSane(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestDNSValidationBadDNSSEC(t *testing.T) {
|
||||
func TestDNSValidationServFail(t *testing.T) {
|
||||
va := NewValidationAuthorityImpl(true)
|
||||
va.DNSResolver = &mocks.MockDNS{}
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
|
|
@ -683,21 +694,21 @@ func TestDNSValidationBadDNSSEC(t *testing.T) {
|
|||
|
||||
chalDNS := core.DNSChallenge()
|
||||
|
||||
badDNSSEC := core.AcmeIdentifier{
|
||||
badIdent := core.AcmeIdentifier{
|
||||
Type: core.IdentifierDNS,
|
||||
Value: "dnssec-failed.org",
|
||||
Value: "servfail.com",
|
||||
}
|
||||
var authz = core.Authorization{
|
||||
ID: core.NewToken(),
|
||||
RegistrationID: 1,
|
||||
Identifier: badDNSSEC,
|
||||
Identifier: badIdent,
|
||||
Challenges: []core.Challenge{chalDNS},
|
||||
}
|
||||
va.validate(authz, 0)
|
||||
|
||||
test.AssertNotNil(t, mockRA.lastAuthz, "Should have gotten an authorization")
|
||||
test.Assert(t, authz.Challenges[0].Status == core.StatusInvalid, "Should be invalid.")
|
||||
test.AssertEquals(t, authz.Challenges[0].Error.Type, core.DNSSECProblem)
|
||||
test.AssertEquals(t, authz.Challenges[0].Error.Type, core.ConnectionProblem)
|
||||
}
|
||||
|
||||
func TestDNSValidationNoServer(t *testing.T) {
|
||||
|
|
@ -717,7 +728,7 @@ func TestDNSValidationNoServer(t *testing.T) {
|
|||
|
||||
test.AssertNotNil(t, mockRA.lastAuthz, "Should have gotten an authorization")
|
||||
test.Assert(t, authz.Challenges[0].Status == core.StatusInvalid, "Should be invalid.")
|
||||
test.AssertEquals(t, authz.Challenges[0].Error.Type, core.ServerInternalProblem)
|
||||
test.AssertEquals(t, authz.Challenges[0].Error.Type, core.ConnectionProblem)
|
||||
}
|
||||
|
||||
// TestDNSValidationLive is an integration test, depending on
|
||||
|
|
|
|||
|
|
@ -121,6 +121,65 @@ func NewWebFrontEndImpl() (WebFrontEndImpl, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
// BodylessResponseWriter wraps http.ResponseWriter, discarding
|
||||
// anything written to the body.
|
||||
type BodylessResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
}
|
||||
|
||||
func (mrw BodylessResponseWriter) Write(buf []byte) (int, error) {
|
||||
return len(buf), nil
|
||||
}
|
||||
|
||||
// HandleFunc registers a handler at the given path. It's
|
||||
// http.HandleFunc(), but with a wrapper around the handler that
|
||||
// provides some generic per-request functionality:
|
||||
//
|
||||
// * Set a Replay-Nonce header.
|
||||
//
|
||||
// * Respond http.StatusMethodNotAllowed for HTTP methods other than
|
||||
// those listed.
|
||||
//
|
||||
// * Never send a body in response to a HEAD request. (Anything
|
||||
// written by the handler will be discarded if the method is HEAD.)
|
||||
func (wfe *WebFrontEndImpl) HandleFunc(mux *http.ServeMux, pattern string, h func(http.ResponseWriter, *http.Request), methods ...string) {
|
||||
methodsOK := make(map[string]bool)
|
||||
for _, m := range methods {
|
||||
methodsOK[m] = true
|
||||
}
|
||||
mux.HandleFunc(pattern, func(response http.ResponseWriter, request *http.Request) {
|
||||
// We do not propagate errors here, because (1) they should be
|
||||
// transient, and (2) they fail closed.
|
||||
nonce, err := wfe.nonceService.Nonce()
|
||||
if err == nil {
|
||||
response.Header().Set("Replay-Nonce", nonce)
|
||||
}
|
||||
response.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
|
||||
switch request.Method {
|
||||
case "HEAD":
|
||||
// We'll be sending an error anyway, but we
|
||||
// should still comply with HTTP spec by not
|
||||
// sending a body.
|
||||
response = BodylessResponseWriter{response}
|
||||
case "OPTIONS":
|
||||
// TODO, #469
|
||||
}
|
||||
|
||||
if _, ok := methodsOK[request.Method]; !ok {
|
||||
logEvent := wfe.populateRequestEvent(request)
|
||||
defer wfe.logRequestDetails(&logEvent)
|
||||
logEvent.Error = "Method not allowed"
|
||||
response.Header().Set("Allow", strings.Join(methods, ", "))
|
||||
wfe.sendError(response, logEvent.Error, request.Method, http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Call the wrapped handler.
|
||||
h(response, request)
|
||||
})
|
||||
}
|
||||
|
||||
// Handler returns an http.Handler that uses various functions for
|
||||
// various ACME-specified paths.
|
||||
func (wfe *WebFrontEndImpl) Handler() http.Handler {
|
||||
|
|
@ -132,17 +191,17 @@ func (wfe *WebFrontEndImpl) Handler() http.Handler {
|
|||
wfe.CertBase = wfe.BaseURL + CertPath
|
||||
|
||||
m := http.NewServeMux()
|
||||
m.HandleFunc("/", wfe.Index)
|
||||
m.HandleFunc(NewRegPath, wfe.NewRegistration)
|
||||
m.HandleFunc(NewAuthzPath, wfe.NewAuthorization)
|
||||
m.HandleFunc(NewCertPath, wfe.NewCertificate)
|
||||
m.HandleFunc(RegPath, wfe.Registration)
|
||||
m.HandleFunc(AuthzPath, wfe.Authorization)
|
||||
m.HandleFunc(CertPath, wfe.Certificate)
|
||||
m.HandleFunc(RevokeCertPath, wfe.RevokeCertificate)
|
||||
m.HandleFunc(TermsPath, wfe.Terms)
|
||||
m.HandleFunc(IssuerPath, wfe.Issuer)
|
||||
m.HandleFunc(BuildIDPath, wfe.BuildID)
|
||||
wfe.HandleFunc(m, "/", wfe.Index, "GET")
|
||||
wfe.HandleFunc(m, NewRegPath, wfe.NewRegistration, "POST")
|
||||
wfe.HandleFunc(m, NewAuthzPath, wfe.NewAuthorization, "POST")
|
||||
wfe.HandleFunc(m, NewCertPath, wfe.NewCertificate, "POST")
|
||||
wfe.HandleFunc(m, RegPath, wfe.Registration, "POST")
|
||||
wfe.HandleFunc(m, AuthzPath, wfe.Authorization, "GET", "POST")
|
||||
wfe.HandleFunc(m, CertPath, wfe.Certificate, "GET", "POST")
|
||||
wfe.HandleFunc(m, RevokeCertPath, wfe.RevokeCertificate, "POST")
|
||||
wfe.HandleFunc(m, TermsPath, wfe.Terms, "GET")
|
||||
wfe.HandleFunc(m, IssuerPath, wfe.Issuer, "GET")
|
||||
wfe.HandleFunc(m, BuildIDPath, wfe.BuildID, "GET")
|
||||
return m
|
||||
}
|
||||
|
||||
|
|
@ -153,8 +212,6 @@ func (wfe *WebFrontEndImpl) Index(response http.ResponseWriter, request *http.Re
|
|||
logEvent := wfe.populateRequestEvent(request)
|
||||
defer wfe.logRequestDetails(&logEvent)
|
||||
|
||||
wfe.sendStandardHeaders(response)
|
||||
|
||||
// http://golang.org/pkg/net/http/#example_ServeMux_Handle
|
||||
// The "/" pattern matches everything, so we need to check
|
||||
// that we're at the root here.
|
||||
|
|
@ -164,13 +221,6 @@ func (wfe *WebFrontEndImpl) Index(response http.ResponseWriter, request *http.Re
|
|||
return
|
||||
}
|
||||
|
||||
if request.Method != "GET" {
|
||||
logEvent.Error = "Method not allowed"
|
||||
sendAllow(response, "GET")
|
||||
wfe.sendError(response, logEvent.Error, request.Method, http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
tmpl := template.Must(template.New("body").Parse(`<html>
|
||||
<body>
|
||||
This is an <a href="https://github.com/letsencrypt/acme-spec/">ACME</a>
|
||||
|
|
@ -189,21 +239,6 @@ func parseIDFromPath(path string) string {
|
|||
return re.ReplaceAllString(path, "")
|
||||
}
|
||||
|
||||
func sendAllow(response http.ResponseWriter, methods ...string) {
|
||||
response.Header().Set("Allow", strings.Join(methods, ", "))
|
||||
}
|
||||
|
||||
func (wfe *WebFrontEndImpl) sendStandardHeaders(response http.ResponseWriter) {
|
||||
// We do not propagate errors here, because (1) they should be
|
||||
// transient, and (2) they fail closed.
|
||||
nonce, err := wfe.nonceService.Nonce()
|
||||
if err == nil {
|
||||
response.Header().Set("Replay-Nonce", nonce)
|
||||
}
|
||||
|
||||
response.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
}
|
||||
|
||||
const (
|
||||
unknownKey = "No registration exists matching provided key"
|
||||
malformedJWS = "Unable to read/verify body"
|
||||
|
|
@ -329,15 +364,6 @@ func (wfe *WebFrontEndImpl) NewRegistration(response http.ResponseWriter, reques
|
|||
logEvent := wfe.populateRequestEvent(request)
|
||||
defer wfe.logRequestDetails(&logEvent)
|
||||
|
||||
wfe.sendStandardHeaders(response)
|
||||
|
||||
if request.Method != "POST" {
|
||||
logEvent.Error = "Method not allowed"
|
||||
sendAllow(response, "POST")
|
||||
wfe.sendError(response, logEvent.Error, "", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
body, key, _, err := wfe.verifyPOST(request, false)
|
||||
if err != nil {
|
||||
logEvent.Error = err.Error()
|
||||
|
|
@ -406,15 +432,6 @@ func (wfe *WebFrontEndImpl) NewAuthorization(response http.ResponseWriter, reque
|
|||
logEvent := wfe.populateRequestEvent(request)
|
||||
defer wfe.logRequestDetails(&logEvent)
|
||||
|
||||
wfe.sendStandardHeaders(response)
|
||||
|
||||
if request.Method != "POST" {
|
||||
logEvent.Error = "Method not allowed"
|
||||
sendAllow(response, "POST")
|
||||
wfe.sendError(response, logEvent.Error, request.Method, http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
body, _, currReg, err := wfe.verifyPOST(request, true)
|
||||
if err != nil {
|
||||
logEvent.Error = err.Error()
|
||||
|
|
@ -484,15 +501,6 @@ func (wfe *WebFrontEndImpl) RevokeCertificate(response http.ResponseWriter, requ
|
|||
logEvent := wfe.populateRequestEvent(request)
|
||||
defer wfe.logRequestDetails(&logEvent)
|
||||
|
||||
wfe.sendStandardHeaders(response)
|
||||
|
||||
if request.Method != "POST" {
|
||||
logEvent.Error = "Method not allowed"
|
||||
sendAllow(response, "POST")
|
||||
wfe.sendError(response, logEvent.Error, request.Method, http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// We don't ask verifyPOST to verify there is a correponding registration,
|
||||
// because anyone with the right private key can revoke a certificate.
|
||||
body, requestKey, registration, err := wfe.verifyPOST(request, false)
|
||||
|
|
@ -585,15 +593,6 @@ func (wfe *WebFrontEndImpl) NewCertificate(response http.ResponseWriter, request
|
|||
logEvent := wfe.populateRequestEvent(request)
|
||||
defer wfe.logRequestDetails(&logEvent)
|
||||
|
||||
wfe.sendStandardHeaders(response)
|
||||
|
||||
if request.Method != "POST" {
|
||||
logEvent.Error = "Method not allowed"
|
||||
sendAllow(response, "POST")
|
||||
wfe.sendError(response, logEvent.Error, request.Method, http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
body, key, reg, err := wfe.verifyPOST(request, true)
|
||||
if err != nil {
|
||||
logEvent.Error = err.Error()
|
||||
|
|
@ -673,15 +672,6 @@ func (wfe *WebFrontEndImpl) NewCertificate(response http.ResponseWriter, request
|
|||
}
|
||||
|
||||
func (wfe *WebFrontEndImpl) challenge(authz core.Authorization, response http.ResponseWriter, request *http.Request, logEvent requestEvent) requestEvent {
|
||||
wfe.sendStandardHeaders(response)
|
||||
|
||||
if request.Method != "GET" && request.Method != "POST" {
|
||||
logEvent.Error = "Method not allowed"
|
||||
sendAllow(response, "GET", "POST")
|
||||
wfe.sendError(response, "Method not allowed", request.Method, http.StatusMethodNotAllowed)
|
||||
return logEvent
|
||||
}
|
||||
|
||||
// Check that the requested challenge exists within the authorization
|
||||
found := false
|
||||
var challengeIndex int
|
||||
|
|
@ -701,12 +691,6 @@ func (wfe *WebFrontEndImpl) challenge(authz core.Authorization, response http.Re
|
|||
}
|
||||
|
||||
switch request.Method {
|
||||
default:
|
||||
logEvent.Error = "Method not allowed"
|
||||
sendAllow(response, "GET", "POST")
|
||||
wfe.sendError(response, logEvent.Error, "", http.StatusMethodNotAllowed)
|
||||
return logEvent
|
||||
|
||||
case "GET":
|
||||
challenge := authz.Challenges[challengeIndex]
|
||||
jsonReply, err := json.Marshal(challenge)
|
||||
|
|
@ -810,15 +794,6 @@ func (wfe *WebFrontEndImpl) Registration(response http.ResponseWriter, request *
|
|||
logEvent := wfe.populateRequestEvent(request)
|
||||
defer wfe.logRequestDetails(&logEvent)
|
||||
|
||||
wfe.sendStandardHeaders(response)
|
||||
|
||||
if request.Method != "POST" {
|
||||
logEvent.Error = "Method not allowed"
|
||||
sendAllow(response, "POST")
|
||||
wfe.sendError(response, logEvent.Error, request.Method, http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
body, _, currReg, err := wfe.verifyPOST(request, true)
|
||||
if err != nil {
|
||||
logEvent.Error = err.Error()
|
||||
|
|
@ -898,15 +873,6 @@ func (wfe *WebFrontEndImpl) Authorization(response http.ResponseWriter, request
|
|||
logEvent := wfe.populateRequestEvent(request)
|
||||
defer wfe.logRequestDetails(&logEvent)
|
||||
|
||||
wfe.sendStandardHeaders(response)
|
||||
|
||||
if request.Method != "GET" && request.Method != "POST" {
|
||||
logEvent.Error = "Method not allowed"
|
||||
sendAllow(response, "GET", "POST")
|
||||
wfe.sendError(response, logEvent.Error, request.Method, http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Requests to this handler should have a path that leads to a known authz
|
||||
id := parseIDFromPath(request.URL.Path)
|
||||
authz, err := wfe.SA.GetAuthorization(id)
|
||||
|
|
@ -929,12 +895,6 @@ func (wfe *WebFrontEndImpl) Authorization(response http.ResponseWriter, request
|
|||
}
|
||||
|
||||
switch request.Method {
|
||||
default:
|
||||
logEvent.Error = "Method not allowed"
|
||||
sendAllow(response, "GET", "POST")
|
||||
wfe.sendError(response, logEvent.Error, request.Method, http.StatusMethodNotAllowed)
|
||||
return
|
||||
|
||||
case "GET":
|
||||
// Blank out ID and regID
|
||||
authz.ID = ""
|
||||
|
|
@ -965,14 +925,6 @@ func (wfe *WebFrontEndImpl) Certificate(response http.ResponseWriter, request *h
|
|||
logEvent := wfe.populateRequestEvent(request)
|
||||
defer wfe.logRequestDetails(&logEvent)
|
||||
|
||||
wfe.sendStandardHeaders(response)
|
||||
|
||||
if request.Method != "GET" && request.Method != "POST" {
|
||||
logEvent.Error = "Method not allowed"
|
||||
sendAllow(response, "GET", "POST")
|
||||
wfe.sendError(response, logEvent.Error, request.Method, http.StatusMethodNotAllowed)
|
||||
}
|
||||
|
||||
path := request.URL.Path
|
||||
switch request.Method {
|
||||
case "GET":
|
||||
|
|
@ -1025,15 +977,6 @@ func (wfe *WebFrontEndImpl) Terms(response http.ResponseWriter, request *http.Re
|
|||
logEvent := wfe.populateRequestEvent(request)
|
||||
defer wfe.logRequestDetails(&logEvent)
|
||||
|
||||
wfe.sendStandardHeaders(response)
|
||||
|
||||
if request.Method != "GET" {
|
||||
logEvent.Error = "Method not allowed"
|
||||
sendAllow(response, "GET")
|
||||
wfe.sendError(response, logEvent.Error, request.Method, http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(response, request, wfe.SubscriberAgreementURL, http.StatusFound)
|
||||
}
|
||||
|
||||
|
|
@ -1042,15 +985,6 @@ func (wfe *WebFrontEndImpl) Issuer(response http.ResponseWriter, request *http.R
|
|||
logEvent := wfe.populateRequestEvent(request)
|
||||
defer wfe.logRequestDetails(&logEvent)
|
||||
|
||||
wfe.sendStandardHeaders(response)
|
||||
|
||||
if request.Method != "GET" {
|
||||
logEvent.Error = "Method not allowed"
|
||||
sendAllow(response, "GET")
|
||||
wfe.sendError(response, "Method not allowed", request.Method, http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO Content negotiation
|
||||
response.Header().Set("Content-Type", "application/pkix-cert")
|
||||
response.WriteHeader(http.StatusOK)
|
||||
|
|
@ -1065,15 +999,6 @@ func (wfe *WebFrontEndImpl) BuildID(response http.ResponseWriter, request *http.
|
|||
logEvent := wfe.populateRequestEvent(request)
|
||||
defer wfe.logRequestDetails(&logEvent)
|
||||
|
||||
wfe.sendStandardHeaders(response)
|
||||
|
||||
if request.Method != "GET" {
|
||||
logEvent.Error = "Method not allowed"
|
||||
sendAllow(response, "GET")
|
||||
wfe.sendError(response, "Method not allowed", request.Method, http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
response.Header().Set("Content-Type", "text/plain")
|
||||
response.WriteHeader(http.StatusOK)
|
||||
detailsString := fmt.Sprintf("Boulder=(%s %s) Golang=(%s) BuildHost=(%s)", core.GetBuildID(), core.GetBuildTime(), runtime.Version(), core.GetBuildHost())
|
||||
|
|
|
|||
|
|
@ -15,9 +15,11 @@ import (
|
|||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log/syslog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
|
@ -27,6 +29,7 @@ import (
|
|||
jose "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/square/go-jose"
|
||||
"github.com/letsencrypt/boulder/core"
|
||||
|
||||
"github.com/letsencrypt/boulder/mocks"
|
||||
"github.com/letsencrypt/boulder/ra"
|
||||
"github.com/letsencrypt/boulder/test"
|
||||
)
|
||||
|
|
@ -124,6 +127,8 @@ wk6Oiadty3eQqSBJv0HnpmiEdQVffIK5Pg4M8Dd+aOBnEkbopAJOuA==
|
|||
"5dd9c885526136d810fc7640f5ba56281e2b75fa3ff7c91a7d23bab7fd4"
|
||||
)
|
||||
|
||||
var log = mocks.UseMockLog()
|
||||
|
||||
type MockSA struct {
|
||||
// empty
|
||||
}
|
||||
|
|
@ -331,30 +336,103 @@ func setupWFE(t *testing.T) WebFrontEndImpl {
|
|||
return wfe
|
||||
}
|
||||
|
||||
func mustParseURL(s string) *url.URL {
|
||||
if u, err := url.Parse(s); err != nil {
|
||||
panic("Cannot parse URL " + s)
|
||||
} else {
|
||||
return u
|
||||
}
|
||||
}
|
||||
|
||||
func sortHeader(s string) string {
|
||||
a := strings.Split(s, ", ")
|
||||
sort.Sort(sort.StringSlice(a))
|
||||
return strings.Join(a, ", ")
|
||||
}
|
||||
|
||||
func TestHandleFunc(t *testing.T) {
|
||||
wfe := setupWFE(t)
|
||||
var mux *http.ServeMux
|
||||
var rw *httptest.ResponseRecorder
|
||||
var stubCalled bool
|
||||
runWrappedHandler := func(req *http.Request, allowed ...string) {
|
||||
mux = http.NewServeMux()
|
||||
rw = httptest.NewRecorder()
|
||||
stubCalled = false
|
||||
wfe.HandleFunc(mux, "/test", func(http.ResponseWriter, *http.Request) {
|
||||
stubCalled = true
|
||||
}, allowed...)
|
||||
req.URL = mustParseURL("/test")
|
||||
mux.ServeHTTP(rw, req)
|
||||
}
|
||||
|
||||
// Plain requests (no CORS)
|
||||
type testCase struct {
|
||||
allowed []string
|
||||
reqMethod string
|
||||
shouldSucceed bool
|
||||
}
|
||||
var lastNonce string
|
||||
for _, c := range []testCase{
|
||||
{[]string{"GET", "POST"}, "GET", true},
|
||||
{[]string{"GET", "POST"}, "POST", true},
|
||||
{[]string{"GET"}, "", false},
|
||||
{[]string{"GET"}, "POST", false},
|
||||
{[]string{"GET"}, "OPTIONS", false}, // TODO, #469
|
||||
{[]string{"GET"}, "MAKE-COFFEE", false}, // 405, or 418?
|
||||
} {
|
||||
runWrappedHandler(&http.Request{Method: c.reqMethod}, c.allowed...)
|
||||
test.AssertEquals(t, stubCalled, c.shouldSucceed)
|
||||
if c.shouldSucceed {
|
||||
test.AssertEquals(t, rw.Code, http.StatusOK)
|
||||
} else {
|
||||
test.AssertEquals(t, rw.Code, http.StatusMethodNotAllowed)
|
||||
test.AssertEquals(t, sortHeader(rw.Header().Get("Allow")), strings.Join(c.allowed, ", "))
|
||||
test.AssertEquals(t,
|
||||
rw.Body.String(),
|
||||
`{"type":"urn:acme:error:malformed","detail":"Method not allowed"}`)
|
||||
}
|
||||
nonce := rw.Header().Get("Replay-Nonce")
|
||||
test.AssertNotEquals(t, nonce, lastNonce)
|
||||
lastNonce = nonce
|
||||
}
|
||||
|
||||
// Disallowed method returns error JSON in body
|
||||
runWrappedHandler(&http.Request{Method: "PUT"}, "GET", "POST")
|
||||
test.AssertEquals(t, rw.Header().Get("Content-Type"), "application/problem+json")
|
||||
test.AssertEquals(t, rw.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Method not allowed"}`)
|
||||
test.AssertEquals(t, sortHeader(rw.Header().Get("Allow")), "GET, POST")
|
||||
|
||||
// Disallowed method special case: response to HEAD has got no body
|
||||
runWrappedHandler(&http.Request{Method: "HEAD"}, "GET", "POST")
|
||||
test.AssertEquals(t, stubCalled, false)
|
||||
test.AssertEquals(t, rw.Body.String(), "")
|
||||
test.AssertEquals(t, sortHeader(rw.Header().Get("Allow")), "GET, POST")
|
||||
}
|
||||
|
||||
func TestStandardHeaders(t *testing.T) {
|
||||
wfe := setupWFE(t)
|
||||
mux := wfe.Handler()
|
||||
|
||||
cases := []struct {
|
||||
path string
|
||||
handler func(http.ResponseWriter, *http.Request)
|
||||
allowed []string
|
||||
}{
|
||||
{"/", wfe.Index, []string{"GET"}},
|
||||
{wfe.NewReg, wfe.NewRegistration, []string{"POST"}},
|
||||
{wfe.RegBase, wfe.Registration, []string{"POST"}},
|
||||
{wfe.NewAuthz, wfe.NewAuthorization, []string{"POST"}},
|
||||
{wfe.AuthzBase, wfe.Authorization, []string{"GET", "POST"}},
|
||||
{wfe.NewCert, wfe.NewCertificate, []string{"POST"}},
|
||||
{wfe.CertBase, wfe.Certificate, []string{"GET", "POST"}},
|
||||
{wfe.SubscriberAgreementURL, wfe.Terms, []string{"GET"}},
|
||||
{"/", []string{"GET"}},
|
||||
{wfe.NewReg, []string{"POST"}},
|
||||
{wfe.RegBase, []string{"POST"}},
|
||||
{wfe.NewAuthz, []string{"POST"}},
|
||||
{wfe.AuthzBase, []string{"GET", "POST"}},
|
||||
{wfe.NewCert, []string{"POST"}},
|
||||
{wfe.CertBase, []string{"GET", "POST"}},
|
||||
{wfe.SubscriberAgreementURL, []string{"GET"}},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
responseWriter := httptest.NewRecorder()
|
||||
url, _ := url.Parse(c.path)
|
||||
c.handler(responseWriter, &http.Request{
|
||||
mux.ServeHTTP(responseWriter, &http.Request{
|
||||
Method: "BOGUS",
|
||||
URL: url,
|
||||
URL: mustParseURL(c.path),
|
||||
})
|
||||
acao := responseWriter.Header().Get("Access-Control-Allow-Origin")
|
||||
nonce := responseWriter.Header().Get("Replay-Nonce")
|
||||
|
|
@ -394,6 +472,7 @@ func TestIndex(t *testing.T) {
|
|||
// - RA returns with a failure
|
||||
func TestIssueCertificate(t *testing.T) {
|
||||
wfe := setupWFE(t)
|
||||
mux := wfe.Handler()
|
||||
|
||||
// TODO: Use a mock RA so we can test various conditions of authorized, not authorized, etc.
|
||||
ra := ra.NewRegistrationAuthorityImpl()
|
||||
|
|
@ -405,8 +484,9 @@ func TestIssueCertificate(t *testing.T) {
|
|||
responseWriter := httptest.NewRecorder()
|
||||
|
||||
// GET instead of POST should be rejected
|
||||
wfe.NewCertificate(responseWriter, &http.Request{
|
||||
mux.ServeHTTP(responseWriter, &http.Request{
|
||||
Method: "GET",
|
||||
URL: mustParseURL(NewCertPath),
|
||||
})
|
||||
test.AssertEquals(t,
|
||||
responseWriter.Body.String(),
|
||||
|
|
@ -498,6 +578,7 @@ func TestIssueCertificate(t *testing.T) {
|
|||
responseWriter.Body.String(),
|
||||
"{\"type\":\"urn:acme:error:unauthorized\",\"detail\":\"Error creating new cert :: Key not authorized for name meep.com\"}")
|
||||
|
||||
log.Clear()
|
||||
responseWriter.Body.Reset()
|
||||
wfe.NewCertificate(responseWriter, &http.Request{
|
||||
Method: "POST",
|
||||
|
|
@ -519,6 +600,11 @@ func TestIssueCertificate(t *testing.T) {
|
|||
test.AssertEquals(
|
||||
t, responseWriter.Header().Get("Content-Type"),
|
||||
"application/pkix-cert")
|
||||
reqlogs := log.GetAllMatching(`Certificate request - successful`)
|
||||
test.AssertEquals(t, len(reqlogs), 1)
|
||||
test.AssertEquals(t, reqlogs[0].Priority, syslog.LOG_NOTICE)
|
||||
test.AssertContains(t, reqlogs[0].Message, `[AUDIT] `)
|
||||
test.AssertContains(t, reqlogs[0].Message, `"Names":["not-an-example.com"]`)
|
||||
}
|
||||
|
||||
func TestChallenge(t *testing.T) {
|
||||
|
|
@ -573,6 +659,7 @@ func TestChallenge(t *testing.T) {
|
|||
|
||||
func TestNewRegistration(t *testing.T) {
|
||||
wfe := setupWFE(t)
|
||||
mux := wfe.Handler()
|
||||
|
||||
wfe.RA = &MockRegistrationAuthority{}
|
||||
wfe.SA = &MockSA{}
|
||||
|
|
@ -581,8 +668,9 @@ func TestNewRegistration(t *testing.T) {
|
|||
responseWriter := httptest.NewRecorder()
|
||||
|
||||
// GET instead of POST should be rejected
|
||||
wfe.NewRegistration(responseWriter, &http.Request{
|
||||
mux.ServeHTTP(responseWriter, &http.Request{
|
||||
Method: "GET",
|
||||
URL: mustParseURL(NewRegPath),
|
||||
})
|
||||
test.AssertEquals(t, responseWriter.Body.String(), "{\"type\":\"urn:acme:error:malformed\",\"detail\":\"Method not allowed\"}")
|
||||
|
||||
|
|
@ -815,6 +903,7 @@ func TestRevokeCertificateAlreadyRevoked(t *testing.T) {
|
|||
|
||||
func TestAuthorization(t *testing.T) {
|
||||
wfe := setupWFE(t)
|
||||
mux := wfe.Handler()
|
||||
|
||||
wfe.RA = &MockRegistrationAuthority{}
|
||||
wfe.SA = &MockSA{}
|
||||
|
|
@ -822,8 +911,9 @@ func TestAuthorization(t *testing.T) {
|
|||
responseWriter := httptest.NewRecorder()
|
||||
|
||||
// GET instead of POST should be rejected
|
||||
wfe.NewAuthorization(responseWriter, &http.Request{
|
||||
mux.ServeHTTP(responseWriter, &http.Request{
|
||||
Method: "GET",
|
||||
URL: mustParseURL(NewAuthzPath),
|
||||
})
|
||||
test.AssertEquals(t, responseWriter.Body.String(), "{\"type\":\"urn:acme:error:malformed\",\"detail\":\"Method not allowed\"}")
|
||||
|
||||
|
|
@ -898,6 +988,7 @@ func TestAuthorization(t *testing.T) {
|
|||
|
||||
func TestRegistration(t *testing.T) {
|
||||
wfe := setupWFE(t)
|
||||
mux := wfe.Handler()
|
||||
|
||||
wfe.RA = &MockRegistrationAuthority{}
|
||||
wfe.SA = &MockSA{}
|
||||
|
|
@ -906,11 +997,10 @@ func TestRegistration(t *testing.T) {
|
|||
responseWriter := httptest.NewRecorder()
|
||||
|
||||
// Test invalid method
|
||||
path, _ := url.Parse("/1")
|
||||
wfe.Registration(responseWriter, &http.Request{
|
||||
mux.ServeHTTP(responseWriter, &http.Request{
|
||||
Method: "MAKE-COFFEE",
|
||||
URL: mustParseURL(RegPath),
|
||||
Body: makeBody("invalid"),
|
||||
URL: path,
|
||||
})
|
||||
test.AssertEquals(t,
|
||||
responseWriter.Body.String(),
|
||||
|
|
@ -918,10 +1008,9 @@ func TestRegistration(t *testing.T) {
|
|||
responseWriter.Body.Reset()
|
||||
|
||||
// Test GET proper entry returns 405
|
||||
path, _ = url.Parse("/1")
|
||||
wfe.Registration(responseWriter, &http.Request{
|
||||
mux.ServeHTTP(responseWriter, &http.Request{
|
||||
Method: "GET",
|
||||
URL: path,
|
||||
URL: mustParseURL(RegPath),
|
||||
})
|
||||
test.AssertEquals(t,
|
||||
responseWriter.Body.String(),
|
||||
|
|
@ -929,7 +1018,7 @@ func TestRegistration(t *testing.T) {
|
|||
responseWriter.Body.Reset()
|
||||
|
||||
// Test POST invalid JSON
|
||||
path, _ = url.Parse("/2")
|
||||
path, _ := url.Parse("/2")
|
||||
wfe.Registration(responseWriter, &http.Request{
|
||||
Method: "POST",
|
||||
Body: makeBody("invalid"),
|
||||
|
|
|
|||
Loading…
Reference in New Issue