diff --git a/akamai/cache-client.go b/akamai/cache-client.go index 05c524d55..2a0325732 100644 --- a/akamai/cache-client.go +++ b/akamai/cache-client.go @@ -205,8 +205,11 @@ func (cpc *CachePurgeClient) purge(urls []string) error { if err != nil { return err } - if purgeInfo.HTTPStatus != 201 || resp.StatusCode != 201 { - return fmt.Errorf("Incorrect HTTP status code: %s", string(body)) + if purgeInfo.HTTPStatus != http.StatusCreated || resp.StatusCode != http.StatusCreated { + if purgeInfo.HTTPStatus == http.StatusForbidden { + return errFatal(fmt.Sprintf("Unauthorized to purge URLs %q", urls)) + } + return fmt.Errorf("Unexpected HTTP status code '%d': %s", resp.StatusCode, string(body)) } cpc.log.Info(fmt.Sprintf( @@ -224,15 +227,15 @@ func (cpc *CachePurgeClient) purge(urls []string) error { func (cpc *CachePurgeClient) Purge(urls []string) error { successful := false for i := 0; i <= cpc.retries; i++ { - core.RetryBackoff(i, cpc.retryBackoff, time.Minute, 1.3) + cpc.clk.Sleep(core.RetryBackoff(i, cpc.retryBackoff, time.Minute, 1.3)) err := cpc.purge(urls) if err != nil { if _, ok := err.(errFatal); ok { - cpc.log.AuditErr(err.Error()) cpc.stats.Inc("FatalFailures", 1) return err } + cpc.log.AuditErr(fmt.Sprintf("Akamai cache purge failed, retrying: %s", err.Error())) cpc.stats.Inc("RetryableFailures", 1) continue } diff --git a/akamai/cache-client_test.go b/akamai/cache-client_test.go index bc16a40e9..1c639e5e3 100644 --- a/akamai/cache-client_test.go +++ b/akamai/cache-client_test.go @@ -2,13 +2,18 @@ package akamai import ( "bytes" + "encoding/json" "fmt" + "io/ioutil" "net/http" + "net/http/httptest" + "strings" "testing" "time" "github.com/jmhodges/clock" + blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/test" ) @@ -49,3 +54,93 @@ func TestConstructAuthHeader(t *testing.T) { test.AssertNotError(t, err, "Failed to create authorization header") test.AssertEquals(t, authHeader, expectedHeader) } + +type akamaiServer struct { + responseCode int +} + +func (as *akamaiServer) akamaiHandler(w http.ResponseWriter, r *http.Request) { + var req struct { + Objects []string + Type string + Action string + } + body, err := ioutil.ReadAll(r.Body) + if err != nil { + fmt.Printf("Failed to read request body: %s\n", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + err = json.Unmarshal(body, &req) + if err != nil { + fmt.Printf("Failed to unmarshal request body: %s\n", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + fmt.Printf("Request: %#v\n", req) + var resp struct { + HTTPStatus int + Detail string + EstimatedSeconds int + PurgeID string + } + resp.HTTPStatus = as.responseCode + resp.Detail = "?" + resp.EstimatedSeconds = 10 + resp.PurgeID = "?" + + for _, testURL := range req.Objects { + if !strings.HasPrefix(testURL, "http://") { + resp.HTTPStatus = http.StatusForbidden + break + } + } + + respBytes, err := json.Marshal(resp) + if err != nil { + fmt.Printf("Failed to marshal response body: %s\n", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(as.responseCode) + w.Write(respBytes) +} + +func TestPurge(t *testing.T) { + log := blog.NewMock() + + as := akamaiServer{responseCode: http.StatusCreated} + m := http.NewServeMux() + server := httptest.NewUnstartedServer(m) + m.HandleFunc("/", as.akamaiHandler) + server.Start() + + client, err := NewCachePurgeClient( + server.URL, + "token", + "secret", + "accessToken", + 3, + time.Second, + log, + metrics.NewNoopScope(), + ) + test.AssertNotError(t, err, "Failed to create CachePurgeClient") + fc := clock.NewFake() + client.clk = fc + + err = client.Purge([]string{"http://test.com"}) + test.AssertNotError(t, err, "Purge failed with 201 response") + + started := fc.Now() + as.responseCode = http.StatusInternalServerError + err = client.Purge([]string{"http://test.com"}) + test.AssertError(t, err, "Purge didn't fail with 400 response") + test.Assert(t, fc.Since(started) > (time.Second*4), "Retries should've taken at least 4.4 seconds") + + started = fc.Now() + as.responseCode = http.StatusCreated + err = client.Purge([]string{"http:/test.com"}) + test.AssertError(t, err, "Purge didn't fail with 403 response from malformed URL") + test.Assert(t, fc.Since(started) < time.Second, "Purge should've failed out immediately") +} diff --git a/cmd/ocsp-updater/main.go b/cmd/ocsp-updater/main.go index 70756ae59..17356107e 100644 --- a/cmd/ocsp-updater/main.go +++ b/cmd/ocsp-updater/main.go @@ -9,7 +9,7 @@ import ( "fmt" "net/url" "os" - "path" + "strings" "time" "github.com/jmhodges/clock" @@ -218,9 +218,12 @@ func (updater *OCSPUpdater) sendPurge(der []byte) { // do GET) urls := []string{} for _, ocspServer := range cert.OCSPServer { + if !strings.HasSuffix(ocspServer, "/") { + ocspServer += "/" + } urls = append( urls, - path.Join(ocspServer, url.QueryEscape(base64.StdEncoding.EncodeToString(req))), + fmt.Sprintf("%s%s", ocspServer, url.QueryEscape(base64.StdEncoding.EncodeToString(req))), ) }