Merge branch 'master' into clean_up_new_reg_test

This commit is contained in:
Jeff Hodges 2015-09-10 14:48:59 -07:00
commit 845e1261a4
11 changed files with 218 additions and 144 deletions

View File

@ -18,7 +18,6 @@ import (
blog "github.com/letsencrypt/boulder/log" blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/ra" "github.com/letsencrypt/boulder/ra"
"github.com/letsencrypt/boulder/rpc" "github.com/letsencrypt/boulder/rpc"
"github.com/letsencrypt/boulder/wfe"
) )
func main() { func main() {
@ -44,7 +43,6 @@ func main() {
cmd.FailOnError(err, "Couldn't create PA") cmd.FailOnError(err, "Couldn't create PA")
rai := ra.NewRegistrationAuthorityImpl(clock.Default(), auditlogger) rai := ra.NewRegistrationAuthorityImpl(clock.Default(), auditlogger)
rai.AuthzBase = c.Common.BaseURL + wfe.AuthzPath
rai.PA = pa rai.PA = pa
raDNSTimeout, err := time.ParseDuration(c.Common.DNSTimeout) raDNSTimeout, err := time.ParseDuration(c.Common.DNSTimeout)
cmd.FailOnError(err, "Couldn't parse RA DNS timeout") cmd.FailOnError(err, "Couldn't parse RA DNS timeout")

View File

@ -241,6 +241,8 @@ type ValidationRecord struct {
// challenge, we just throw all the elements into one bucket, // challenge, we just throw all the elements into one bucket,
// together with the common metadata elements. // together with the common metadata elements.
type Challenge struct { type Challenge struct {
ID int64 `json:"id,omitempty"`
// The type of challenge // The type of challenge
Type string `json:"type"` Type string `json:"type"`
@ -255,7 +257,7 @@ type Challenge struct {
Validated *time.Time `json:"validated,omitempty"` Validated *time.Time `json:"validated,omitempty"`
// A URI to which a response can be POSTed // A URI to which a response can be POSTed
URI *AcmeURL `json:"uri"` URI string `json:"uri"`
// Used by simpleHttp, dvsni, and dns challenges // Used by simpleHttp, dvsni, and dns challenges
Token string `json:"token,omitempty"` Token string `json:"token,omitempty"`
@ -431,6 +433,18 @@ type Authorization struct {
Combinations [][]int `json:"combinations,omitempty" db:"combinations"` Combinations [][]int `json:"combinations,omitempty" db:"combinations"`
} }
// FindChallenge will look for the given challenge inside this authorization. If
// found, it will return the index of that challenge within the Authorization's
// Challenges array. Otherwise it will return -1.
func (authz *Authorization) FindChallenge(challengeID int64) int {
for i, c := range authz.Challenges {
if c.ID == challengeID {
return i
}
}
return -1
}
// JSONBuffer fields get encoded and decoded JOSE-style, in base64url encoding // JSONBuffer fields get encoded and decoded JOSE-style, in base64url encoding
// with stripped padding. // with stripped padding.
type JSONBuffer []byte type JSONBuffer []byte

View File

@ -10,7 +10,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/mail" "net/mail"
"strconv"
"strings" "strings"
"time" "time"
@ -31,8 +30,6 @@ type RegistrationAuthorityImpl struct {
DNSResolver core.DNSResolver DNSResolver core.DNSResolver
clk clock.Clock clk clock.Clock
log *blog.AuditLogger log *blog.AuditLogger
AuthzBase string
} }
// NewRegistrationAuthorityImpl constructs a new RA object. // NewRegistrationAuthorityImpl constructs a new RA object.
@ -150,6 +147,11 @@ func (ra *RegistrationAuthorityImpl) NewAuthorization(request core.Authorization
// Create validations, but we have to update them with URIs later // Create validations, but we have to update them with URIs later
challenges, combinations := ra.PA.ChallengesFor(identifier) challenges, combinations := ra.PA.ChallengesFor(identifier)
for i, _ := range challenges {
// Add the account key used to generate the challenge
challenges[i].AccountKey = &reg.Key
}
// Partially-filled object // Partially-filled object
authz = core.Authorization{ authz = core.Authorization{
Identifier: identifier, Identifier: identifier,
@ -165,33 +167,19 @@ func (ra *RegistrationAuthorityImpl) NewAuthorization(request core.Authorization
// InternalServerError since the user-data was validated before being // InternalServerError since the user-data was validated before being
// passed to the SA. // passed to the SA.
err = core.InternalServerError(fmt.Sprintf("Invalid authorization request: %s", err)) err = core.InternalServerError(fmt.Sprintf("Invalid authorization request: %s", err))
return authz, err return core.Authorization{}, err
} }
// Construct all the challenge URIs // Check each challenge for sanity.
for i := range authz.Challenges { for _, challenge := range authz.Challenges {
// Ignoring these errors because we construct the URLs to be correct if !challenge.IsSane(false) {
challengeURI, _ := core.ParseAcmeURL(ra.AuthzBase + authz.ID + "?challenge=" + strconv.Itoa(i))
authz.Challenges[i].URI = challengeURI
// Add the account key used to generate the challenge
authz.Challenges[i].AccountKey = &reg.Key
if !authz.Challenges[i].IsSane(false) {
// InternalServerError because we generated these challenges, they should // InternalServerError because we generated these challenges, they should
// be OK. // be OK.
err = core.InternalServerError(fmt.Sprintf("Challenge didn't pass sanity check: %+v", authz.Challenges[i])) err = core.InternalServerError(fmt.Sprintf("Challenge didn't pass sanity check: %+v", challenge))
return authz, err return core.Authorization{}, err
} }
} }
// Store the authorization object, then return it
err = ra.SA.UpdatePendingAuthorization(authz)
if err != nil {
// InternalServerError because we created the authorization just above,
// and adding Sane challenges should not break it.
err = core.InternalServerError(err.Error())
}
return authz, err return authz, err
} }

View File

@ -214,7 +214,6 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, *sa.SQLStorageAut
ra.VA = va ra.VA = va
ra.CA = &ca ra.CA = &ca
ra.PA = pa ra.PA = pa
ra.AuthzBase = "http://acme.invalid/authz/"
ra.DNSResolver = &mocks.MockDNS{} ra.DNSResolver = &mocks.MockDNS{}
AuthzInitial.RegistrationID = Registration.ID AuthzInitial.RegistrationID = Registration.ID

View File

@ -366,6 +366,7 @@ func (rpc *AmqpRPCServer) processMessage(msg amqp.Delivery) {
CorrelationId: msg.CorrelationId, CorrelationId: msg.CorrelationId,
Type: msg.Type, Type: msg.Type,
Body: jsonResponse, // XXX-JWS: jws.Sign(privKey, body) Body: jsonResponse, // XXX-JWS: jws.Sign(privKey, body)
Expiration: "30000",
}) })
} }
@ -486,7 +487,12 @@ func NewAmqpRPCClient(clientQueuePrefix, serverQueue string, channel *amqp.Chann
return nil, err return nil, err
} }
clientQueue := fmt.Sprintf("%s.%s", clientQueuePrefix, hostname) randID := make([]byte, 3)
_, err = rand.Read(randID)
if err != nil {
return nil, err
}
clientQueue := fmt.Sprintf("%s.%s.%x", clientQueuePrefix, hostname, randID)
rpc = &AmqpRPCCLient{ rpc = &AmqpRPCCLient{
serverQueue: serverQueue, serverQueue: serverQueue,
@ -558,6 +564,7 @@ func (rpc *AmqpRPCCLient) Dispatch(method string, body []byte) chan []byte {
ReplyTo: rpc.clientQueue, ReplyTo: rpc.clientQueue,
Type: method, Type: method,
Body: body, // XXX-JWS: jws.Sign(privKey, body) Body: body, // XXX-JWS: jws.Sign(privKey, body)
Expiration: "30000",
}) })
return responseChan return responseChan

View File

@ -0,0 +1,13 @@
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
ALTER TABLE `challenges` DROP COLUMN `uri`;
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back
ALTER TABLE `challenges` ADD COLUMN (
`uri` varchar(255)
);

View File

@ -36,7 +36,6 @@ type challModel struct {
Status core.AcmeStatus `db:"status"` Status core.AcmeStatus `db:"status"`
Error []byte `db:"error"` Error []byte `db:"error"`
Validated *time.Time `db:"validated"` Validated *time.Time `db:"validated"`
URI string `db:"uri"`
Token string `db:"token"` Token string `db:"token"`
TLS *bool `db:"tls"` TLS *bool `db:"tls"`
Validation []byte `db:"validation"` Validation []byte `db:"validation"`
@ -85,6 +84,7 @@ func modelToRegistration(rm *regModel) (core.Registration, error) {
func challengeToModel(c *core.Challenge, authID string) (*challModel, error) { func challengeToModel(c *core.Challenge, authID string) (*challModel, error) {
cm := challModel{ cm := challModel{
ID: c.ID,
AuthorizationID: authID, AuthorizationID: authID,
Type: c.Type, Type: c.Type,
Status: c.Status, Status: c.Status,
@ -108,12 +108,6 @@ func challengeToModel(c *core.Challenge, authID string) (*challModel, error) {
} }
cm.Error = errJSON cm.Error = errJSON
} }
if c.URI != nil {
if len(c.URI.String()) > 255 {
return nil, fmt.Errorf("URI is too long to store in the database")
}
cm.URI = c.URI.String()
}
if len(c.ValidationRecord) > 0 { if len(c.ValidationRecord) > 0 {
vrJSON, err := json.Marshal(c.ValidationRecord) vrJSON, err := json.Marshal(c.ValidationRecord)
if err != nil { if err != nil {
@ -139,19 +133,13 @@ func challengeToModel(c *core.Challenge, authID string) (*challModel, error) {
func modelToChallenge(cm *challModel) (core.Challenge, error) { func modelToChallenge(cm *challModel) (core.Challenge, error) {
c := core.Challenge{ c := core.Challenge{
ID: cm.ID,
Type: cm.Type, Type: cm.Type,
Status: cm.Status, Status: cm.Status,
Validated: cm.Validated, Validated: cm.Validated,
Token: cm.Token, Token: cm.Token,
TLS: cm.TLS, TLS: cm.TLS,
} }
if len(cm.URI) > 0 {
uri, err := core.ParseAcmeURL(cm.URI)
if err != nil {
return core.Challenge{}, err
}
c.URI = uri
}
if len(cm.Validation) > 0 { if len(cm.Validation) > 0 {
val, err := jose.ParseSigned(string(cm.Validation)) val, err := jose.ParseSigned(string(cm.Validation))
if err != nil { if err != nil {

View File

@ -440,17 +440,28 @@ func (ssa *SQLStorageAuthority) NewPendingAuthorization(authz core.Authorization
return return
} }
for _, c := range authz.Challenges { for i, c := range authz.Challenges {
chall, err := challengeToModel(&c, pendingAuthz.ID) challModel, err := challengeToModel(&c, pendingAuthz.ID)
if err != nil { if err != nil {
tx.Rollback() tx.Rollback()
return core.Authorization{}, err return core.Authorization{}, err
} }
err = tx.Insert(chall) // Magic happens here: Gorp will modify challModel, setting challModel.ID
// to the auto-increment primary key. This is important because we want
// the challenge objects inside the Authorization we return to know their
// IDs, so they can have proper URLs.
// See https://godoc.org/github.com/coopernurse/gorp#DbMap.Insert
err = tx.Insert(challModel)
if err != nil { if err != nil {
tx.Rollback() tx.Rollback()
return core.Authorization{}, err return core.Authorization{}, err
} }
challenge, err := modelToChallenge(challModel)
if err != nil {
tx.Rollback()
return core.Authorization{}, err
}
authz.Challenges[i] = challenge
} }
err = tx.Commit() err = tx.Commit()

View File

@ -177,9 +177,7 @@ func CreateDomainAuthWithRegId(t *testing.T, domainName string, sa *SQLStorageAu
test.Assert(t, authz.ID != "", "ID shouldn't be blank") test.Assert(t, authz.ID != "", "ID shouldn't be blank")
// prepare challenge for auth // prepare challenge for auth
u, err := core.ParseAcmeURL(domainName) chall := core.Challenge{Type: "simpleHttp", Status: core.StatusValid, URI: domainName, Token: "THISWOULDNTBEAGOODTOKEN"}
test.AssertNotError(t, err, "Couldn't parse domainName "+domainName)
chall := core.Challenge{Type: "simpleHttp", Status: core.StatusValid, URI: u, Token: "THISWOULDNTBEAGOODTOKEN"}
combos := make([][]int, 1) combos := make([][]int, 1)
combos[0] = []int{0, 1} combos[0] = []int{0, 1}
exp := time.Now().AddDate(0, 0, 1) // expire in 1 day exp := time.Now().AddDate(0, 0, 1) // expire in 1 day

View File

@ -11,7 +11,6 @@ import (
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
"html/template"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"regexp" "regexp"
@ -32,6 +31,7 @@ const (
RegPath = "/acme/reg/" RegPath = "/acme/reg/"
NewAuthzPath = "/acme/new-authz" NewAuthzPath = "/acme/new-authz"
AuthzPath = "/acme/authz/" AuthzPath = "/acme/authz/"
ChallengePath = "/acme/challenge/"
NewCertPath = "/acme/new-cert" NewCertPath = "/acme/new-cert"
CertPath = "/acme/cert/" CertPath = "/acme/cert/"
RevokeCertPath = "/acme/revoke-cert" RevokeCertPath = "/acme/revoke-cert"
@ -47,13 +47,14 @@ type WebFrontEndImpl struct {
log *blog.AuditLogger log *blog.AuditLogger
// URL configuration parameters // URL configuration parameters
BaseURL string BaseURL string
NewReg string NewReg string
RegBase string RegBase string
NewAuthz string NewAuthz string
AuthzBase string AuthzBase string
NewCert string ChallengeBase string
CertBase string NewCert string
CertBase string
// JSON encoded endpoint directory // JSON encoded endpoint directory
DirectoryJSON []byte DirectoryJSON []byte
@ -195,6 +196,7 @@ func (wfe *WebFrontEndImpl) Handler() (http.Handler, error) {
wfe.RegBase = wfe.BaseURL + RegPath wfe.RegBase = wfe.BaseURL + RegPath
wfe.NewAuthz = wfe.BaseURL + NewAuthzPath wfe.NewAuthz = wfe.BaseURL + NewAuthzPath
wfe.AuthzBase = wfe.BaseURL + AuthzPath wfe.AuthzBase = wfe.BaseURL + AuthzPath
wfe.ChallengeBase = wfe.BaseURL + ChallengePath
wfe.NewCert = wfe.BaseURL + NewCertPath wfe.NewCert = wfe.BaseURL + NewCertPath
wfe.CertBase = wfe.BaseURL + CertPath wfe.CertBase = wfe.BaseURL + CertPath
@ -212,18 +214,22 @@ func (wfe *WebFrontEndImpl) Handler() (http.Handler, error) {
wfe.DirectoryJSON = directoryJSON wfe.DirectoryJSON = directoryJSON
m := http.NewServeMux() m := http.NewServeMux()
wfe.HandleFunc(m, "/", wfe.Index, "GET")
wfe.HandleFunc(m, DirectoryPath, wfe.Directory, "GET") wfe.HandleFunc(m, DirectoryPath, wfe.Directory, "GET")
wfe.HandleFunc(m, NewRegPath, wfe.NewRegistration, "POST") wfe.HandleFunc(m, NewRegPath, wfe.NewRegistration, "POST")
wfe.HandleFunc(m, NewAuthzPath, wfe.NewAuthorization, "POST") wfe.HandleFunc(m, NewAuthzPath, wfe.NewAuthorization, "POST")
wfe.HandleFunc(m, NewCertPath, wfe.NewCertificate, "POST") wfe.HandleFunc(m, NewCertPath, wfe.NewCertificate, "POST")
wfe.HandleFunc(m, RegPath, wfe.Registration, "POST") wfe.HandleFunc(m, RegPath, wfe.Registration, "POST")
wfe.HandleFunc(m, AuthzPath, wfe.Authorization, "GET", "POST") wfe.HandleFunc(m, AuthzPath, wfe.Authorization, "GET")
wfe.HandleFunc(m, ChallengePath, wfe.Challenge, "GET", "POST")
wfe.HandleFunc(m, CertPath, wfe.Certificate, "GET") wfe.HandleFunc(m, CertPath, wfe.Certificate, "GET")
wfe.HandleFunc(m, RevokeCertPath, wfe.RevokeCertificate, "POST") wfe.HandleFunc(m, RevokeCertPath, wfe.RevokeCertificate, "POST")
wfe.HandleFunc(m, TermsPath, wfe.Terms, "GET") wfe.HandleFunc(m, TermsPath, wfe.Terms, "GET")
wfe.HandleFunc(m, IssuerPath, wfe.Issuer, "GET") wfe.HandleFunc(m, IssuerPath, wfe.Issuer, "GET")
wfe.HandleFunc(m, BuildIDPath, wfe.BuildID, "GET") wfe.HandleFunc(m, BuildIDPath, wfe.BuildID, "GET")
// We don't use our special HandleFunc for "/" because it matches everything,
// meaning we can wind up returning 405 when we mean to return 404. See
// https://github.com/letsencrypt/boulder/issues/717
m.HandleFunc("/", wfe.Index)
return m, nil return m, nil
} }
@ -243,16 +249,22 @@ func (wfe *WebFrontEndImpl) Index(response http.ResponseWriter, request *http.Re
return return
} }
tmpl := template.Must(template.New("body").Parse(`<html> if request.Method != "GET" {
<body> logEvent.Error = "Bad method"
This is an <a href="https://github.com/letsencrypt/acme-spec/">ACME</a> response.Header().Set("Allow", "GET")
Certificate Authority running <a href="https://github.com/letsencrypt/boulder">Boulder</a>, response.WriteHeader(http.StatusMethodNotAllowed)
New registration is available at <a href="{{.NewReg}}">{{.NewReg}}</a>. return
</body> }
</html>
`))
tmpl.Execute(response, wfe)
response.Header().Set("Content-Type", "text/html") response.Header().Set("Content-Type", "text/html")
response.Write([]byte(fmt.Sprintf(`<html>
<body>
This is an <a href="https://github.com/letsencrypt/acme-spec/">ACME</a>
Certificate Authority running <a href="https://github.com/letsencrypt/boulder">Boulder</a>.
JSON directory is available at <a href="%s">%s</a>.
</body>
</html>
`, DirectoryPath, DirectoryPath)))
addCacheHeader(response, wfe.IndexCacheDuration.Seconds()) addCacheHeader(response, wfe.IndexCacheDuration.Seconds())
} }
@ -265,6 +277,7 @@ func addCacheHeader(w http.ResponseWriter, age float64) {
} }
func (wfe *WebFrontEndImpl) Directory(response http.ResponseWriter, request *http.Request) { func (wfe *WebFrontEndImpl) Directory(response http.ResponseWriter, request *http.Request) {
response.Header().Set("Content-Type", "application/json")
response.Write(wfe.DirectoryJSON) response.Write(wfe.DirectoryJSON)
} }
@ -554,8 +567,7 @@ func (wfe *WebFrontEndImpl) NewAuthorization(response http.ResponseWriter, reque
// Make a URL for this authz, then blow away the ID and RegID before serializing // Make a URL for this authz, then blow away the ID and RegID before serializing
authzURL := wfe.AuthzBase + string(authz.ID) authzURL := wfe.AuthzBase + string(authz.ID)
authz.ID = "" wfe.prepAuthorizationForDisplay(&authz)
authz.RegistrationID = 0
responseBody, err := json.Marshal(authz) responseBody, err := json.Marshal(authz)
if err != nil { if err != nil {
logEvent.Error = err.Error() logEvent.Error = err.Error()
@ -773,46 +785,93 @@ func (wfe *WebFrontEndImpl) NewCertificate(response http.ResponseWriter, request
wfe.Stats.Inc("Certificates", 1, 1.0) wfe.Stats.Inc("Certificates", 1, 1.0)
} }
func (wfe *WebFrontEndImpl) challenge( func (wfe *WebFrontEndImpl) Challenge(
response http.ResponseWriter, response http.ResponseWriter,
request *http.Request, request *http.Request) {
authz core.Authorization, logEvent := wfe.populateRequestEvent(request)
logEvent *requestEvent) { defer wfe.logRequestDetails(&logEvent)
// Check that the requested challenge exists within the authorization notFound := func() {
found := false wfe.sendError(response, "No such registration", request.URL.Path, http.StatusNotFound)
var challengeIndex int
var challenge core.Challenge
for i, challenge := range authz.Challenges {
tempURL := challenge.URI
if tempURL.Path == request.URL.Path && tempURL.RawQuery == request.URL.RawQuery {
found = true
challengeIndex = i
break
}
} }
if !found { // Challenge URIs are of the form /acme/challenge/<auth id>/<challenge id>.
logEvent.Error = "Unable to find challenge" // Here we parse out the id components. TODO: Use a better tool to parse out
wfe.sendError(response, logEvent.Error, request.URL.RawQuery, http.StatusNotFound) // URL structure: https://github.com/letsencrypt/boulder/issues/437
slug := strings.Split(request.URL.Path[len(ChallengePath):], "/")
if len(slug) != 2 {
notFound()
return return
} }
authorizationID := slug[0]
challengeID, err := strconv.ParseInt(slug[1], 10, 64)
if err != nil {
notFound()
return
}
logEvent.Extra["AuthorizationID"] = authorizationID
logEvent.Extra["ChallengeID"] = challengeID
authz, err := wfe.SA.GetAuthorization(authorizationID)
if err != nil {
notFound()
return
}
// Check that the requested challenge exists within the authorization
challengeIndex := authz.FindChallenge(challengeID)
if challengeIndex == -1 {
notFound()
return
}
challenge := authz.Challenges[challengeIndex]
logEvent.Extra["ChallengeType"] = challenge.Type
logEvent.Extra["AuthorizationRegistrationID"] = authz.RegistrationID
logEvent.Extra["AuthorizationIdentifier"] = authz.Identifier
logEvent.Extra["AuthorizationStatus"] = authz.Status
logEvent.Extra["AuthorizationExpires"] = authz.Expires
switch request.Method { switch request.Method {
case "GET": case "GET":
wfe.getChallenge(response, request, authz, challenge, logEvent) wfe.getChallenge(response, request, authz, &challenge, &logEvent)
case "POST": case "POST":
wfe.postChallenge(response, request, authz, challengeIndex, logEvent) wfe.postChallenge(response, request, authz, challengeIndex, &logEvent)
} }
} }
// prepChallengeForDisplay takes a core.Challenge and prepares it for display to
// the client by filling in its URI field and clearing its AccountKey and ID
// fields.
// TODO: Come up with a cleaner way to do this.
// https://github.com/letsencrypt/boulder/issues/761
func (wfe *WebFrontEndImpl) prepChallengeForDisplay(authz core.Authorization, challenge *core.Challenge) {
challenge.URI = fmt.Sprintf("%s%s/%d", wfe.ChallengeBase, authz.ID, challenge.ID)
challenge.AccountKey = nil
// 0 is considered "empty" for the purpose of the JSON omitempty tag.
challenge.ID = 0
}
// prepAuthorizationForDisplay takes a core.Authorization and prepares it for
// display to the client by clearing its ID and RegistrationID fields, and
// preparing all its challenges.
func (wfe *WebFrontEndImpl) prepAuthorizationForDisplay(authz *core.Authorization) {
for i, _ := range authz.Challenges {
wfe.prepChallengeForDisplay(*authz, &authz.Challenges[i])
}
authz.ID = ""
authz.RegistrationID = 0
}
func (wfe *WebFrontEndImpl) getChallenge( func (wfe *WebFrontEndImpl) getChallenge(
response http.ResponseWriter, response http.ResponseWriter,
request *http.Request, request *http.Request,
authz core.Authorization, authz core.Authorization,
challenge core.Challenge, challenge *core.Challenge,
logEvent *requestEvent) { logEvent *requestEvent) {
wfe.prepChallengeForDisplay(authz, challenge)
jsonReply, err := json.Marshal(challenge) jsonReply, err := json.Marshal(challenge)
if err != nil { if err != nil {
logEvent.Error = err.Error() logEvent.Error = err.Error()
@ -823,7 +882,7 @@ func (wfe *WebFrontEndImpl) getChallenge(
} }
authzURL := wfe.AuthzBase + string(authz.ID) authzURL := wfe.AuthzBase + string(authz.ID)
response.Header().Add("Location", challenge.URI.String()) response.Header().Add("Location", challenge.URI)
response.Header().Set("Content-Type", "application/json") response.Header().Set("Content-Type", "application/json")
response.Header().Add("Link", link(authzURL, "up")) response.Header().Add("Link", link(authzURL, "up"))
response.WriteHeader(http.StatusAccepted) response.WriteHeader(http.StatusAccepted)
@ -890,6 +949,7 @@ func (wfe *WebFrontEndImpl) postChallenge(
// assumption: UpdateAuthorization does not modify order of challenges // assumption: UpdateAuthorization does not modify order of challenges
challenge := updatedAuthorization.Challenges[challengeIndex] challenge := updatedAuthorization.Challenges[challengeIndex]
wfe.prepChallengeForDisplay(authz, &challenge)
jsonReply, err := json.Marshal(challenge) jsonReply, err := json.Marshal(challenge)
if err != nil { if err != nil {
logEvent.Error = err.Error() logEvent.Error = err.Error()
@ -899,7 +959,7 @@ func (wfe *WebFrontEndImpl) postChallenge(
} }
authzURL := wfe.AuthzBase + string(authz.ID) authzURL := wfe.AuthzBase + string(authz.ID)
response.Header().Add("Location", challenge.URI.String()) response.Header().Add("Location", challenge.URI)
response.Header().Set("Content-Type", "application/json") response.Header().Set("Content-Type", "application/json")
response.Header().Add("Link", link(authzURL, "up")) response.Header().Add("Link", link(authzURL, "up"))
response.WriteHeader(http.StatusAccepted) response.WriteHeader(http.StatusAccepted)
@ -1013,31 +1073,8 @@ func (wfe *WebFrontEndImpl) Authorization(response http.ResponseWriter, request
logEvent.Extra["AuthorizationStatus"] = authz.Status logEvent.Extra["AuthorizationStatus"] = authz.Status
logEvent.Extra["AuthorizationExpires"] = authz.Expires logEvent.Extra["AuthorizationExpires"] = authz.Expires
// If there is a fragment, then this is actually a request to a challenge URI wfe.prepAuthorizationForDisplay(&authz)
if len(request.URL.RawQuery) != 0 {
wfe.challenge(response, request, authz, &logEvent)
} else if request.Method == "GET" {
wfe.GetAuthorization(response, request, authz, &logEvent)
} else {
// For challenges, POST and GET are allowed. For authorizations only GET is
// allowed.
// TODO(jsha): Split challenge updates into a different path so we can use
// the HandleFunc functionality for declaring allowed methods.
// https://github.com/letsencrypt/boulder/issues/638
logEvent.Error = "Method not allowed"
response.Header().Set("Allow", "GET")
wfe.sendError(response, logEvent.Error, request.Method, http.StatusMethodNotAllowed)
}
}
func (wfe *WebFrontEndImpl) GetAuthorization(
response http.ResponseWriter,
request *http.Request,
authz core.Authorization,
logEvent *requestEvent) {
// Blank out ID and regID
authz.ID = ""
authz.RegistrationID = 0
jsonReply, err := json.Marshal(authz) jsonReply, err := json.Marshal(authz)
if err != nil { if err != nil {
logEvent.Error = err.Error() logEvent.Error = err.Error()

View File

@ -156,7 +156,20 @@ func (sa *MockSA) GetRegistrationByKey(jwk jose.JsonWebKey) (core.Registration,
func (sa *MockSA) GetAuthorization(id string) (core.Authorization, error) { func (sa *MockSA) GetAuthorization(id string) (core.Authorization, error) {
if id == "valid" { if id == "valid" {
exp := time.Now().AddDate(100, 0, 0) exp := time.Now().AddDate(100, 0, 0)
return core.Authorization{Status: core.StatusValid, RegistrationID: 1, Expires: &exp, Identifier: core.AcmeIdentifier{Type: "dns", Value: "not-an-example.com"}}, nil return core.Authorization{
ID: "valid",
Status: core.StatusValid,
RegistrationID: 1,
Expires: &exp,
Identifier: core.AcmeIdentifier{Type: "dns", Value: "not-an-example.com"},
Challenges: []core.Challenge{
core.Challenge{
ID: 23,
Type: "dns",
URI: "http://localhost:4300/acme/challenge/valid/23",
},
},
}, nil
} }
return core.Authorization{}, nil return core.Authorization{}, nil
} }
@ -343,6 +356,7 @@ func setupWFE(t *testing.T) WebFrontEndImpl {
wfe.RegBase = wfe.BaseURL + RegPath wfe.RegBase = wfe.BaseURL + RegPath
wfe.NewAuthz = wfe.BaseURL + NewAuthzPath wfe.NewAuthz = wfe.BaseURL + NewAuthzPath
wfe.AuthzBase = wfe.BaseURL + AuthzPath wfe.AuthzBase = wfe.BaseURL + AuthzPath
wfe.ChallengeBase = wfe.BaseURL + ChallengePath
wfe.NewCert = wfe.BaseURL + NewCertPath wfe.NewCert = wfe.BaseURL + NewCertPath
wfe.CertBase = wfe.BaseURL + CertPath wfe.CertBase = wfe.BaseURL + CertPath
wfe.SubscriberAgreementURL = agreementURL wfe.SubscriberAgreementURL = agreementURL
@ -458,11 +472,11 @@ func TestStandardHeaders(t *testing.T) {
path string path string
allowed []string allowed []string
}{ }{
{"/", []string{"GET"}},
{wfe.NewReg, []string{"POST"}}, {wfe.NewReg, []string{"POST"}},
{wfe.RegBase, []string{"POST"}}, {wfe.RegBase, []string{"POST"}},
{wfe.NewAuthz, []string{"POST"}}, {wfe.NewAuthz, []string{"POST"}},
{wfe.AuthzBase, []string{"GET", "POST"}}, {wfe.AuthzBase, []string{"GET"}},
{wfe.ChallengeBase, []string{"GET", "POST"}},
{wfe.NewCert, []string{"POST"}}, {wfe.NewCert, []string{"POST"}},
{wfe.CertBase, []string{"GET"}}, {wfe.CertBase, []string{"GET"}},
{wfe.SubscriberAgreementURL, []string{"GET"}}, {wfe.SubscriberAgreementURL, []string{"GET"}},
@ -484,6 +498,28 @@ func TestStandardHeaders(t *testing.T) {
} }
} }
func TestIndexPOST(t *testing.T) {
wfe := setupWFE(t)
responseWriter := httptest.NewRecorder()
url, _ := url.Parse("/")
wfe.Index(responseWriter, &http.Request{
Method: "POST",
URL: url,
})
test.AssertEquals(t, responseWriter.Code, http.StatusMethodNotAllowed)
}
func TestPOST404(t *testing.T) {
wfe := setupWFE(t)
responseWriter := httptest.NewRecorder()
url, _ := url.Parse("/foobar")
wfe.Index(responseWriter, &http.Request{
Method: "POST",
URL: url,
})
test.AssertEquals(t, responseWriter.Code, http.StatusNotFound)
}
func TestIndex(t *testing.T) { func TestIndex(t *testing.T) {
wfe := setupWFE(t) wfe := setupWFE(t)
wfe.IndexCacheDuration = time.Second * 10 wfe.IndexCacheDuration = time.Second * 10
@ -497,8 +533,8 @@ func TestIndex(t *testing.T) {
}) })
test.AssertEquals(t, responseWriter.Code, http.StatusOK) test.AssertEquals(t, responseWriter.Code, http.StatusOK)
test.AssertNotEquals(t, responseWriter.Body.String(), "404 page not found\n") test.AssertNotEquals(t, responseWriter.Body.String(), "404 page not found\n")
test.Assert(t, strings.Contains(responseWriter.Body.String(), wfe.NewReg), test.Assert(t, strings.Contains(responseWriter.Body.String(), DirectoryPath),
"new-reg not found") "directory path not found")
test.AssertEquals(t, responseWriter.Header().Get("Cache-Control"), "public, max-age=10") test.AssertEquals(t, responseWriter.Header().Get("Cache-Control"), "public, max-age=10")
responseWriter.Body.Reset() responseWriter.Body.Reset()
@ -525,6 +561,7 @@ func TestDirectory(t *testing.T) {
Method: "GET", Method: "GET",
URL: url, URL: url,
}) })
test.AssertEquals(t, responseWriter.Header().Get("Content-Type"), "application/json")
test.AssertEquals(t, responseWriter.Code, http.StatusOK) test.AssertEquals(t, responseWriter.Code, http.StatusOK)
test.AssertEquals(t, responseWriter.Body.String(), `{"new-authz":"http://localhost:4300/acme/new-authz","new-cert":"http://localhost:4300/acme/new-cert","new-reg":"http://localhost:4300/acme/new-reg","revoke-cert":"http://localhost:4300/acme/revoke-cert"}`) test.AssertEquals(t, responseWriter.Body.String(), `{"new-authz":"http://localhost:4300/acme/new-authz","new-cert":"http://localhost:4300/acme/new-cert","new-reg":"http://localhost:4300/acme/new-reg","revoke-cert":"http://localhost:4300/acme/revoke-cert"}`)
} }
@ -675,37 +712,21 @@ func TestChallenge(t *testing.T) {
`), &key) `), &key)
test.AssertNotError(t, err, "Could not unmarshal testing key") test.AssertNotError(t, err, "Could not unmarshal testing key")
challengeURL := "/acme/authz/asdf?challenge=foo" challengeURL := "/acme/challenge/valid/23"
challengeAcme := (*core.AcmeURL)(mustParseURL(challengeURL)) wfe.Challenge(responseWriter,
authz := core.Authorization{
ID: "asdf",
Identifier: core.AcmeIdentifier{
Type: "dns",
Value: "letsencrypt.org",
},
Challenges: []core.Challenge{
core.Challenge{
Type: "dns",
URI: challengeAcme,
},
},
RegistrationID: 1,
}
wfe.challenge(responseWriter,
makePostRequestWithPath(challengeURL, makePostRequestWithPath(challengeURL,
signRequest(t, `{"resource":"challenge"}`, &wfe.nonceService)), signRequest(t, `{"resource":"challenge"}`, &wfe.nonceService)))
authz, &requestEvent{})
test.AssertEquals(t, responseWriter.Code, 202)
test.AssertEquals( test.AssertEquals(
t, responseWriter.Header().Get("Location"), t, responseWriter.Header().Get("Location"),
"/acme/authz/asdf?challenge=foo") challengeURL)
test.AssertEquals( test.AssertEquals(
t, responseWriter.Header().Get("Link"), t, responseWriter.Header().Get("Link"),
`</acme/authz/asdf>;rel="up"`) `</acme/authz/valid>;rel="up"`)
test.AssertEquals( test.AssertEquals(
t, responseWriter.Body.String(), t, responseWriter.Body.String(),
`{"type":"dns","uri":"/acme/authz/asdf?challenge=foo"}`) `{"type":"dns","uri":"/acme/challenge/valid/23"}`)
} }
func TestNewRegistration(t *testing.T) { func TestNewRegistration(t *testing.T) {