Remove Akamai CCU v2 support (#3994)

Fixes #3991.
This commit is contained in:
Roland Bracewell Shoemaker 2019-01-08 12:28:11 -08:00 committed by GitHub
parent b0f407dcf0
commit cdef80ce67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 27 additions and 143 deletions

View File

@ -21,17 +21,10 @@ import (
)
const (
v2PurgePath = "/ccu/v2/queues/default"
v3PurgePath = "/ccu/v3/delete/url/"
timestampFormat = "20060102T15:04:05-0700"
)
type v2PurgeRequest struct {
Objects []string `json:"objects"`
Type string `json:"type"`
Action string `json:"action"`
}
type v3PurgeRequest struct {
Objects []string `json:"objects"`
}
@ -44,9 +37,7 @@ type purgeResponse struct {
}
// CachePurgeClient talks to the Akamai CCU REST API. It is safe to make concurrent
// purge requests. If the v3Network field is "" the legacy CCU v2 API is used.
// If the v3Network field is either "staging" or "production" then the CCU v3
// API is used.
// purge requests.
type CachePurgeClient struct {
client *http.Client
apiEndpoint string
@ -95,9 +86,8 @@ func NewCachePurgeClient(
if err != nil {
return nil, err
}
// If there is a network provided, we're using the V3 API and the network
// string must be either "production" or "staging".
if v3Network != "" && v3Network != "production" && v3Network != "staging" {
// The network string must be either "production" or "staging".
if v3Network != "production" && v3Network != "staging" {
return nil, fmt.Errorf(
"Invalid CCU v3 network: %q. Must be \"staging\" or \"production\"", v3Network)
}
@ -121,7 +111,7 @@ func NewCachePurgeClient(
// Akamai uses a special authorization header to identify clients to their EdgeGrid
// APIs, their docs (https://developer.akamai.com/introduction/Client_Auth.html)
// provide a description of the required generation process.
func (cpc *CachePurgeClient) constructAuthHeader(request *http.Request, body []byte, apiPath string, nonce string) (string, error) {
func (cpc *CachePurgeClient) constructAuthHeader(body []byte, apiPath string, nonce string) (string, error) {
// The akamai API is very time sensitive (recommending reliance on a stratum 2
// or better time source) and, although it doesn't say it anywhere, really wants
// the timestamp to be in the UTC timezone for some reason.
@ -169,23 +159,11 @@ func signingKey(clientSecret string, timestamp string) []byte {
// purge actually sends the individual requests to the Akamai endpoint and checks
// if they are successful
func (cpc *CachePurgeClient) purge(urls []string) error {
var purgeReq interface{}
var endpoint, purgePath string
if cpc.v3Network == "" {
purgeReq = v2PurgeRequest{
Objects: urls,
Action: "remove",
Type: "arl",
}
purgePath = v2PurgePath
endpoint = fmt.Sprintf("%s%s", cpc.apiEndpoint, purgePath)
} else {
purgeReq = v3PurgeRequest{
Objects: urls,
}
purgePath = v3PurgePath
endpoint = fmt.Sprintf("%s%s%s", cpc.apiEndpoint, purgePath, cpc.v3Network)
purgeReq := v3PurgeRequest{
Objects: urls,
}
endpoint := fmt.Sprintf("%s%s%s", cpc.apiEndpoint, v3PurgePath, cpc.v3Network)
reqJSON, err := json.Marshal(purgeReq)
if err != nil {
return errFatal(err.Error())
@ -201,9 +179,8 @@ func (cpc *CachePurgeClient) purge(urls []string) error {
// Create authorization header for request
authHeader, err := cpc.constructAuthHeader(
req,
reqJSON,
purgePath+cpc.v3Network,
v3PurgePath+cpc.v3Network,
core.RandomString(16),
)
if err != nil {

View File

@ -1,7 +1,6 @@
package akamai
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
@ -38,16 +37,8 @@ func TestConstructAuthHeader(t *testing.T) {
test.AssertNotError(t, err, "Failed to parse timestamp")
fc.Set(wantedTimestamp)
req, err := http.NewRequest(
"POST",
fmt.Sprintf("%s%s", cpc.apiEndpoint, v2PurgePath),
bytes.NewBuffer([]byte{0}),
)
test.AssertNotError(t, err, "Failed to create request")
expectedHeader := "EG1-HMAC-SHA256 client_token=akab-client-token-xxx-xxxxxxxxxxxxxxxx;access_token=akab-access-token-xxx-xxxxxxxxxxxxxxxx;timestamp=20140321T19:34:21+0000;nonce=nonce-xx-xxxx-xxxx-xxxx-xxxxxxxxxxxx;signature=hXm4iCxtpN22m4cbZb4lVLW5rhX8Ca82vCFqXzSTPe4="
authHeader, err := cpc.constructAuthHeader(
req,
[]byte("datadatadatadatadatadatadatadata"),
"/testapi/v1/t3",
"nonce-xx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
@ -58,7 +49,6 @@ func TestConstructAuthHeader(t *testing.T) {
type akamaiServer struct {
responseCode int
v3 bool
*httptest.Server
}
@ -74,9 +64,7 @@ func (as *akamaiServer) sendResponse(w http.ResponseWriter, resp purgeResponse)
}
func (as *akamaiServer) akamaiHandler(w http.ResponseWriter, r *http.Request) {
// Enforce the request path based on the API version we're emulating
if (as.v3 == false && r.URL.Path != v2PurgePath) ||
(as.v3 == true && !strings.HasPrefix(r.URL.Path, v3PurgePath)) {
if !strings.HasPrefix(r.URL.Path, v3PurgePath) {
resp := purgeResponse{
HTTPStatus: http.StatusNotFound,
Detail: fmt.Sprintf("Invalid path: %q", r.URL.Path),
@ -87,8 +75,6 @@ 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 {
@ -104,17 +90,6 @@ func (as *akamaiServer) akamaiHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Enforce that a V3 request is well formed and does not include the "Type"
// and "Action" fields used by the V2 api.
if as.v3 == true && (req.Type != "" || req.Action != "") {
resp := purgeResponse{
HTTPStatus: http.StatusBadRequest,
Detail: fmt.Sprintf("Invalid request body: included V2 Type %q and Action %q\n", req.Type, req.Action),
}
as.sendResponse(w, resp)
return
}
err = json.Unmarshal(body, &req)
if err != nil {
fmt.Printf("Failed to unmarshal request body: %s\n", err)
@ -137,60 +112,19 @@ func (as *akamaiServer) akamaiHandler(w http.ResponseWriter, r *http.Request) {
}
as.sendResponse(w, resp)
}
func newAkamaiServer(code int, v3 bool) *akamaiServer {
func newAkamaiServer(code int) *akamaiServer {
m := http.NewServeMux()
as := akamaiServer{
responseCode: code,
v3: v3,
Server: httptest.NewServer(m),
}
m.HandleFunc("/", as.akamaiHandler)
return &as
}
// TestV2Purge tests the legacy CCU v2 Akamai API used when the v3Network
// parameter to NewCachePurgeClient is "".
func TestV2Purge(t *testing.T) {
log := blog.NewMock()
as := newAkamaiServer(http.StatusCreated, false)
defer as.Close()
client, err := NewCachePurgeClient(
as.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")
}
// TestV3Purge tests the Akamai CCU v3 purge API by setting the v3Network
// parameter to "production".
// TestV3Purge tests the Akamai CCU v3 purge API
func TestV3Purge(t *testing.T) {
as := newAkamaiServer(http.StatusCreated, true)
as := newAkamaiServer(http.StatusCreated)
defer as.Close()
// Client is a purge client with a "production" v3Network parameter
@ -275,7 +209,6 @@ func TestBigBatchPurge(t *testing.T) {
m := http.NewServeMux()
as := akamaiServer{
responseCode: http.StatusCreated,
v3: true,
Server: httptest.NewUnstartedServer(m),
}
m.HandleFunc("/", as.akamaiHandler)

View File

@ -172,13 +172,10 @@ type OCSPUpdaterConfig struct {
OldestIssuedSCT ConfigDuration
ParallelGenerateOCSPRequests int
AkamaiBaseURL string
AkamaiClientToken string
AkamaiClientSecret string
AkamaiAccessToken string
// When AkamaiV3Network is not provided, the Akamai CCU API v2 is used. When
// AkamaiV3Network is set to "staging" or "production" the Akamai CCU API v3
// is used.
AkamaiBaseURL string
AkamaiClientToken string
AkamaiClientSecret string
AkamaiAccessToken string
AkamaiV3Network string
AkamaiPurgeRetries int
AkamaiPurgeRetryBackoff ConfigDuration

View File

@ -18,8 +18,6 @@ func main() {
secret := flag.String("secret", "", "Akamai client secret")
flag.Parse()
// v2
v2Purges := [][]string{}
v3Purges := [][]string{}
mu := sync.Mutex{}
@ -27,9 +25,8 @@ func main() {
mu.Lock()
defer mu.Unlock()
body, err := json.Marshal(struct {
V2 [][]string
V3 [][]string
}{V2: v2Purges, V3: v3Purges})
}{V3: v3Purges})
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
@ -41,13 +38,11 @@ func main() {
http.HandleFunc("/debug/reset-purges", func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
v2Purges, v3Purges = [][]string{}, [][]string{}
v3Purges = [][]string{}
w.WriteHeader(http.StatusOK)
return
})
// Since v2 and v3 APIs share a bunch of logic just mash them into a single
// handler.
http.HandleFunc("/ccu/", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
@ -58,8 +53,6 @@ func main() {
defer mu.Unlock()
var purgeRequest struct {
Objects []string `json:"objects"`
Type string `json:"type"`
Action string `json:"action"`
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
@ -77,21 +70,12 @@ func main() {
fmt.Println("Can't unmarshal:", err)
return
}
if r.URL.Path == "/ccu/v2/queues/default" {
if purgeRequest.Type != "arl" || purgeRequest.Action != "remove" || len(purgeRequest.Objects) == 0 {
w.WriteHeader(http.StatusBadRequest)
fmt.Println("Bad parameters:", purgeRequest)
return
}
v2Purges = append(v2Purges, purgeRequest.Objects)
} else if r.URL.Path == "/ccu/v3/delete/url/staging" {
if len(purgeRequest.Objects) == 0 || purgeRequest.Type != "" || purgeRequest.Action != "" {
w.WriteHeader(http.StatusBadRequest)
fmt.Println("Bad parameters:", purgeRequest)
return
}
v3Purges = append(v3Purges, purgeRequest.Objects)
if len(purgeRequest.Objects) == 0 {
w.WriteHeader(http.StatusBadRequest)
fmt.Println("Bad parameters:", purgeRequest)
return
}
v3Purges = append(v3Purges, purgeRequest.Objects)
respObj := struct {
PurgeID string

View File

@ -21,6 +21,7 @@
"akamaiClientToken": "its-a-token",
"akamaiClientSecret": "its-a-secret",
"akamaiAccessToken": "idk-how-this-is-different-from-client-token-but-okay",
"akamaiV3Network": "staging",
"tls": {
"caCertFile": "test/grpc-creds/minica.pem",
"certFile": "test/grpc-creds/ocsp-updater.boulder/cert.pem",

View File

@ -115,14 +115,6 @@ def reset_akamai_purges():
def verify_akamai_purge():
response = requests.get("http://localhost:6789/debug/get-purges")
purgeData = response.json()
if os.environ.get('BOULDER_CONFIG_DIR', '').startswith("test/config-next"):
if len(purgeData["V3"]) is not 1:
raise Exception("Unexpected number of Akamai v3 purges")
if len(purgeData["V2"]) is not 0:
raise Exception("Unexpected number of Akamai v2 purges")
else:
if len(purgeData["V2"]) is not 1:
raise Exception("Unexpected number of Akamai v2 purges")
if len(purgeData["V3"]) is not 0:
raise Exception("Unexpected number of Akamai v3 purges")
if len(purgeData["V3"]) is not 1:
raise Exception("Unexpected number of Akamai v3 purges")
reset_akamai_purges()