parent
b0f407dcf0
commit
cdef80ce67
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in New Issue