mirror of
https://github.com/wahyd4/cert-manager.git
synced 2026-08-09 05:06:38 +10:00
Update Aka issuer to use v2 API
Signed-off-by: Edward Lynes <elynes@akamai.com>
This commit is contained in:
@@ -15,6 +15,7 @@ require (
|
||||
github.com/Azure/go-autorest/autorest/to v0.4.0
|
||||
github.com/Azure/go-autorest/autorest/validation v0.3.0 // indirect
|
||||
github.com/Venafi/vcert/v4 v4.13.1
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.1.0
|
||||
github.com/aws/aws-sdk-go v1.34.30
|
||||
github.com/cloudflare/cloudflare-go v0.13.2
|
||||
github.com/cpu/goacmedns v0.0.3
|
||||
|
||||
@@ -73,6 +73,8 @@ github.com/Venafi/vcert/v4 v4.13.1 h1:T+ZAygGBhncKGUFCtBth2rXrokJkGv8cOeEOgx9XLz
|
||||
github.com/Venafi/vcert/v4 v4.13.1/go.mod h1:Z3sJFoAurFNXPpoSUSHq46aIeHLiGQEMDhprfxlpofQ=
|
||||
github.com/agnivade/levenshtein v1.0.1 h1:3oJU7J3FGFmyhn8KHjmVaZCN5hxTr7GxgRue+sxIXdQ=
|
||||
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.1.0 h1:NNsy0ugDFZbu6S11NlsR4Kmko9hJ00gv9U2JHbwpf3g=
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.1.0/go.mod h1:kX6YddBkXqqywAe8c9LyvgTCyFuZCTMF4cRPQhc3Fy8=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
@@ -379,6 +381,7 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.5 h1:UImYN5qQ8tuGpGE16ZmjvcTtTw24zw1QAp/SlnNrZhI=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||
github.com/hashicorp/consul/api v1.1.0 h1:BNQPM9ytxj6jbjjdRPioQ94T6YXriSopn0i8COv6SRA=
|
||||
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
|
||||
github.com/hashicorp/consul/sdk v0.1.1 h1:LnuDWGNsoajlhGyHJvuWW6FVqRl8JOTPqS6CPTsYjhY=
|
||||
@@ -555,6 +558,7 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223 h1:F9x/1yl3T2
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
|
||||
@@ -582,6 +586,8 @@ github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQ
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pavel-v-chernykh/keystore-go v2.1.0+incompatible h1:Jd6xfriVlJ6hWPvYOE0Ni0QWcNTLRehfGPFxr3eSL80=
|
||||
github.com/pavel-v-chernykh/keystore-go v2.1.0+incompatible/go.mod h1:xlUlxe/2ItGlQyMTstqeDv9r3U4obH7xYd26TbDQutY=
|
||||
github.com/pavel-v-chernykh/keystore-go/v4 v4.1.0 h1:xKxUVGoB9VJU+lgQLPN0KURjw+XCVVSpHfQEeyxk3zo=
|
||||
@@ -707,6 +713,9 @@ github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
|
||||
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
||||
github.com/vektah/gqlparser v1.1.2 h1:ZsyLGn7/7jDNI+y4SEhI4yAxRChlv15pUHMjijT+e68=
|
||||
github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1 h1:j2hhcujLRHAg872RWAV5yaUrEjHEObwDv3aImCaNLek=
|
||||
@@ -966,9 +975,11 @@ gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.52.0 h1:j+Lt/M1oPPejkniCg1TkWE2J3Eh1oZTsHSXzMTzUXn4=
|
||||
gopkg.in/ini.v1 v1.52.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
|
||||
|
||||
@@ -59,6 +59,15 @@ def go_repositories():
|
||||
version = "v1.0.1",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_akamai_AkamaiOPEN-edgegrid-golang",
|
||||
build_file_generation = "on",
|
||||
build_file_proto_mode = "disable",
|
||||
importpath = "github.com/akamai/AkamaiOPEN-edgegrid-golang",
|
||||
sum = "h1:NNsy0ugDFZbu6S11NlsR4Kmko9hJ00gv9U2JHbwpf3g=",
|
||||
version = "v1.1.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_alecthomas_template",
|
||||
build_file_generation = "on",
|
||||
|
||||
@@ -2,16 +2,14 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"akamai.go",
|
||||
"edgegridauth.go",
|
||||
],
|
||||
srcs = ["akamai.go"],
|
||||
importpath = "github.com/jetstack/cert-manager/pkg/issuer/acme/dns/akamai",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//pkg/issuer/acme/dns/util:go_default_library",
|
||||
"//pkg/logs:go_default_library",
|
||||
"//pkg/util:go_default_library",
|
||||
"@com_github_akamai_AkamaiOPEN-edgegrid-golang//configdns-v2:go_default_library",
|
||||
"@com_github_akamai_AkamaiOPEN-edgegrid-golang//edgegrid:go_default_library",
|
||||
"@com_github_go_logr_logr//:go_default_library",
|
||||
"@com_github_pkg_errors//:go_default_library",
|
||||
],
|
||||
@@ -19,13 +17,11 @@ go_library(
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"akamai_test.go",
|
||||
"edgegridauth_test.go",
|
||||
],
|
||||
srcs = ["akamai_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/issuer/acme/dns/util:go_default_library",
|
||||
"@com_github_akamai_AkamaiOPEN-edgegrid-golang//configdns-v2:go_default_library",
|
||||
"@com_github_stretchr_testify//assert:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -15,51 +15,76 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
// Package akamai implements a DNS provider for solving the DNS-01
|
||||
// challenge using Akamai FastDNS.
|
||||
// See https://developer.akamai.com/api/luna/config-dns/overview.html
|
||||
// challenge using Akamai Edge DNS.
|
||||
// See https://developer.akamai.com/api/cloud_security/edge_dns_zone_management/v2.html
|
||||
|
||||
package akamai
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
dns "github.com/akamai/AkamaiOPEN-edgegrid-golang/configdns-v2"
|
||||
"github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/jetstack/cert-manager/pkg/issuer/acme/dns/util"
|
||||
logf "github.com/jetstack/cert-manager/pkg/logs"
|
||||
pkgutil "github.com/jetstack/cert-manager/pkg/util"
|
||||
)
|
||||
|
||||
// Interface defined to enable mocking and required functions
|
||||
type OpenEdgegridDNSService interface {
|
||||
GetRecord(zone string, name string, record_type string) (*dns.RecordBody, error)
|
||||
RecordSave(rec *dns.RecordBody, zone string) error
|
||||
RecordUpdate(rec *dns.RecordBody, zone string) error
|
||||
RecordDelete(rec *dns.RecordBody, zone string) error
|
||||
}
|
||||
|
||||
type OpenDNSConfig struct {
|
||||
config edgegrid.Config
|
||||
}
|
||||
|
||||
// DNSProvider is an implementation of the acme.ChallengeProvider interface
|
||||
type DNSProvider struct {
|
||||
dns01Nameservers []string
|
||||
// serviceConsumerDomain as issued by Akamai Luna Control Center.
|
||||
// The ServiceConsumerDomain is the base URL.
|
||||
serviceConsumerDomain string
|
||||
|
||||
auth *EdgeGridAuth
|
||||
|
||||
transport http.RoundTripper
|
||||
dns01Nameservers []string
|
||||
serviceConsumerDomain string
|
||||
dnsclient OpenEdgegridDNSService
|
||||
TTL int
|
||||
findHostedDomainByFqdn func(string, []string) (string, error)
|
||||
isNotFound func(error) bool
|
||||
log logr.Logger
|
||||
}
|
||||
|
||||
// NewDNSProvider returns a DNSProvider instance configured for Akamai.
|
||||
func NewDNSProvider(serviceConsumerDomain, clientToken, clientSecret, accessToken string, dns01Nameservers []string) (*DNSProvider, error) {
|
||||
return &DNSProvider{
|
||||
dns01Nameservers,
|
||||
serviceConsumerDomain,
|
||||
NewEdgeGridAuth(clientToken, clientSecret, accessToken),
|
||||
http.DefaultTransport,
|
||||
findHostedDomainByFqdn,
|
||||
logf.Log.WithName("akamai-dns"),
|
||||
}, nil
|
||||
|
||||
// required Aka OpenEdgegrid creds + non empty dnsservers list
|
||||
if serviceConsumerDomain == "" || clientToken == "" || clientSecret == "" || accessToken == "" || len(dns01Nameservers) < 1 {
|
||||
return nil, fmt.Errorf("edgedns: Provider creation failed. Missing required arguments.")
|
||||
}
|
||||
|
||||
dnsp := &DNSProvider{
|
||||
dns01Nameservers: dns01Nameservers,
|
||||
serviceConsumerDomain: serviceConsumerDomain,
|
||||
dnsclient: &OpenDNSConfig{},
|
||||
findHostedDomainByFqdn: findHostedDomainByFqdn,
|
||||
isNotFound: isNotFound,
|
||||
log: logf.Log.WithName("akamai-dns"),
|
||||
TTL: 300,
|
||||
}
|
||||
dnsp.dnsclient.(*OpenDNSConfig).config = edgegrid.Config{
|
||||
Host: serviceConsumerDomain,
|
||||
ClientToken: clientToken,
|
||||
ClientSecret: clientSecret,
|
||||
AccessToken: accessToken,
|
||||
MaxBody: 131072,
|
||||
}
|
||||
|
||||
dns.Init(dnsp.dnsclient.(*OpenDNSConfig).config)
|
||||
|
||||
return dnsp, nil
|
||||
}
|
||||
|
||||
func findHostedDomainByFqdn(fqdn string, ns []string) (string, error) {
|
||||
@@ -71,218 +96,186 @@ func findHostedDomainByFqdn(fqdn string, ns []string) (string, error) {
|
||||
return util.UnFqdn(zone), nil
|
||||
}
|
||||
|
||||
// Present creates a TXT record to fulfil the dns-01 challenge
|
||||
// Present creates/updates a TXT record to fulfill the dns-01 challenge.
|
||||
func (a *DNSProvider) Present(domain, fqdn, value string) error {
|
||||
return a.setTxtRecord(fqdn, &dns01Record{value, 60})
|
||||
}
|
||||
|
||||
// CleanUp removes the TXT record matching the specified parameters
|
||||
func (a *DNSProvider) CleanUp(domain, fqdn, value string) error {
|
||||
return a.setTxtRecord(fqdn, nil)
|
||||
}
|
||||
logf.V(logf.DebugLevel).Infof("entering Present. domain: %s, fqdn: %s, value: %s", domain, fqdn, value)
|
||||
|
||||
type dns01Record struct {
|
||||
value string
|
||||
ttl int
|
||||
}
|
||||
|
||||
func (a *DNSProvider) setTxtRecord(fqdn string, dns01Record *dns01Record) error {
|
||||
hostedDomain, err := a.findHostedDomainByFqdn(fqdn, a.dns01Nameservers)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to determine hosted domain for %q", fqdn)
|
||||
}
|
||||
|
||||
zoneData, err := a.loadZoneData(hostedDomain)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to load zone data for %q", hostedDomain)
|
||||
return errors.Wrapf(err, "edgedns: failed to determine hosted domain for %q", fqdn)
|
||||
}
|
||||
hostedDomain = util.UnFqdn(hostedDomain)
|
||||
logf.V(logf.DebugLevel).Infof("hostedDomain: %s", hostedDomain)
|
||||
|
||||
recordName, err := makeTxtRecordName(fqdn, hostedDomain)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to create TXT record name")
|
||||
return errors.Wrapf(err, "edgedns: failed to create TXT record name")
|
||||
}
|
||||
logf.V(logf.DebugLevel).Infof("recordName: %s", recordName)
|
||||
|
||||
record, err := a.dnsclient.GetRecord(hostedDomain, recordName, "TXT")
|
||||
if err != nil && !a.isNotFound(err) {
|
||||
return errors.Wrapf(err, "edgedns: failed to retrieve TXT record")
|
||||
}
|
||||
|
||||
if updated, err := zoneData.setTxtRecord(recordName, dns01Record); !updated || err != nil {
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to set TXT record in %q", hostedDomain)
|
||||
if err == nil && record == nil {
|
||||
return fmt.Errorf("edgedns: unknown error")
|
||||
}
|
||||
|
||||
if record != nil {
|
||||
logf.V(logf.InfoLevel).Infof("edgedns: TXT record already exists. Updating target")
|
||||
|
||||
if containsValue(record.Target, value) {
|
||||
// have a record and have entry already
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.Errorf("no %q TXT record found in %q", recordName, hostedDomain)
|
||||
record.Target = append(record.Target, `"`+value+`"`)
|
||||
record.TTL = a.TTL
|
||||
|
||||
err = a.dnsclient.RecordUpdate(record, hostedDomain)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "edgedns: failed to update TXT record")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
newSerial, err := zoneData.incSoaSerial()
|
||||
record = &dns.RecordBody{
|
||||
Name: recordName,
|
||||
RecordType: "TXT",
|
||||
TTL: a.TTL,
|
||||
Target: []string{`"` + value + `"`},
|
||||
}
|
||||
|
||||
err = a.dnsclient.RecordSave(record, hostedDomain)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to increment SOA serial for %q", hostedDomain)
|
||||
return errors.Wrapf(err, "edgedns: failed to create TXT record")
|
||||
}
|
||||
|
||||
if err := a.saveZoneData(hostedDomain, zoneData); err != nil {
|
||||
return errors.Wrapf(err, "failed to save zone data for %q", hostedDomain)
|
||||
}
|
||||
|
||||
logf.V(logf.DebugLevel).Infof("Updated Akamai TXT record for %q on %q using SOA serial of %d", recordName, hostedDomain, newSerial)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanUp removes/updates the TXT record matching the specified parameters.
|
||||
func (a *DNSProvider) CleanUp(domain, fqdn, value string) error {
|
||||
|
||||
logf.V(logf.DebugLevel).Infof("entering CleanUp. domain: %s, fqdn: %s, value: %s", domain, fqdn, value)
|
||||
|
||||
hostedDomain, err := a.findHostedDomainByFqdn(fqdn, a.dns01Nameservers)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "edgedns: failed to determine hosted domain for %q", fqdn)
|
||||
}
|
||||
hostedDomain = util.UnFqdn(hostedDomain)
|
||||
logf.V(logf.DebugLevel).Infof("hostedDomain: %s", hostedDomain)
|
||||
|
||||
recordName, err := makeTxtRecordName(fqdn, hostedDomain)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "edgedns: failed to create TXT record name")
|
||||
}
|
||||
logf.V(logf.DebugLevel).Infof("recordName: %s", recordName)
|
||||
|
||||
existingRec, err := a.dnsclient.GetRecord(hostedDomain, recordName, "TXT")
|
||||
if err != nil {
|
||||
if a.isNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return errors.Wrapf(err, "edgedns: failed to retrieve TXT record")
|
||||
}
|
||||
|
||||
if existingRec == nil {
|
||||
return fmt.Errorf("edgedns: unknown failure")
|
||||
}
|
||||
|
||||
if len(existingRec.Target) == 0 {
|
||||
return fmt.Errorf("edgedns: TXT record is invalid")
|
||||
}
|
||||
|
||||
if !containsValue(existingRec.Target, value) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var newRData []string
|
||||
for _, val := range existingRec.Target {
|
||||
tval := strings.Trim(val, `"`)
|
||||
if tval == value {
|
||||
continue
|
||||
}
|
||||
newRData = append(newRData, val)
|
||||
}
|
||||
|
||||
if len(newRData) > 0 {
|
||||
existingRec.Target = newRData
|
||||
logf.V(logf.DebugLevel).Infof("updating Akamai TXT record: %s, data: %s", existingRec.Name, newRData)
|
||||
err = a.dnsclient.RecordUpdate(existingRec, hostedDomain)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "edgedns: TXT record update failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
logf.V(logf.DebugLevel).Infof("deleting Akamai TXT record %s", existingRec.Name)
|
||||
err = a.dnsclient.RecordDelete(existingRec, hostedDomain)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "edgedns: TXT record delete failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func containsValue(values []string, value string) bool {
|
||||
for _, val := range values {
|
||||
if strings.Trim(val, `"`) == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func isNotFound(err error) bool {
|
||||
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
_, ok := err.(*dns.RecordError)
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func makeTxtRecordName(fqdn, hostedDomain string) (string, error) {
|
||||
if !strings.HasSuffix(fqdn, "."+hostedDomain+".") {
|
||||
|
||||
recName := util.UnFqdn(fqdn)
|
||||
if !strings.HasSuffix(recName, hostedDomain) {
|
||||
return "", errors.Errorf("fqdn %q is not part of %q", fqdn, hostedDomain)
|
||||
}
|
||||
|
||||
return fqdn[0 : len(fqdn)-len(hostedDomain)-2], nil
|
||||
return recName, nil
|
||||
}
|
||||
|
||||
func (a *DNSProvider) urlForDomain(domain string) string {
|
||||
return fmt.Sprintf("https://%s/config-dns/v1/zones/%s", a.serviceConsumerDomain, domain)
|
||||
func (o OpenDNSConfig) GetRecord(zone string, name string, record_type string) (*dns.RecordBody, error) {
|
||||
|
||||
dns.Config = o.config
|
||||
|
||||
return dns.GetRecord(zone, name, record_type)
|
||||
}
|
||||
|
||||
func (a *DNSProvider) loadZoneData(domain string) (zoneData, error) {
|
||||
url := a.urlForDomain(domain)
|
||||
req, err := http.NewRequest(http.MethodGet, url, http.NoBody)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create HTTP request")
|
||||
}
|
||||
func (o OpenDNSConfig) RecordSave(rec *dns.RecordBody, zone string) error {
|
||||
|
||||
responsePayload, err := a.makeRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var zoneData map[string]interface{}
|
||||
err = json.NewDecoder(bytes.NewReader(responsePayload)).Decode(&zoneData)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to decode Akamai OPEN API response")
|
||||
}
|
||||
|
||||
return zoneData, nil
|
||||
return rec.Save(zone)
|
||||
}
|
||||
|
||||
func (a *DNSProvider) saveZoneData(domain string, data zoneData) error {
|
||||
body, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to encode zone data")
|
||||
}
|
||||
func (o OpenDNSConfig) RecordUpdate(rec *dns.RecordBody, zone string) error {
|
||||
|
||||
url := a.urlForDomain(domain)
|
||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create HTTP request")
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
if _, err := a.makeRequest(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return rec.Update(zone)
|
||||
}
|
||||
|
||||
func (a *DNSProvider) makeRequest(req *http.Request) ([]byte, error) {
|
||||
req.Header.Set("User-Agent", pkgutil.CertManagerUserAgent)
|
||||
func (o OpenDNSConfig) RecordDelete(rec *dns.RecordBody, zone string) error {
|
||||
|
||||
if err := a.auth.SignRequest(req); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to sign HTTP request")
|
||||
}
|
||||
|
||||
client := http.Client{
|
||||
Transport: a.transport,
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error querying Akamai OPEN API")
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNoContent {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("unexpected response from Akamai OPEN API: status_code: %d, status: %s", resp.StatusCode, resp.Status)
|
||||
}
|
||||
|
||||
responsePayload, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read response payload")
|
||||
}
|
||||
|
||||
return responsePayload, nil
|
||||
}
|
||||
|
||||
type zoneData map[string]interface{}
|
||||
|
||||
func (z zoneData) setTxtRecord(name string, dns01Record *dns01Record) (bool, error) {
|
||||
zone, ok := z["zone"].(map[string]interface{})
|
||||
if !ok {
|
||||
return false, errors.New("failed to retrieve zone from zone data")
|
||||
}
|
||||
|
||||
var txtRecords []interface{}
|
||||
if txtNode, ok := zone["txt"]; ok {
|
||||
if txtRecords, ok = txtNode.([]interface{}); !ok {
|
||||
return false, errors.New("failed to retrieve TXT records from zone data")
|
||||
}
|
||||
}
|
||||
|
||||
if dns01Record == nil {
|
||||
if txtRecords = deleteRecord(txtRecords, name); txtRecords == nil {
|
||||
return false, nil
|
||||
}
|
||||
} else {
|
||||
txtRecords = updateRecord(txtRecords, name, map[string]interface{}{
|
||||
"name": name,
|
||||
"ttl": dns01Record.ttl,
|
||||
"active": true,
|
||||
"target": dns01Record.value,
|
||||
})
|
||||
}
|
||||
|
||||
if len(txtRecords) < 1 {
|
||||
delete(zone, "txt")
|
||||
} else {
|
||||
zone["txt"] = txtRecords
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (z zoneData) incSoaSerial() (uint64, error) {
|
||||
soa, ok := z["zone"].(map[string]interface{})["soa"].(map[string]interface{})
|
||||
if !ok {
|
||||
return 0, errors.New("failed to retrieve SOA record from zone data")
|
||||
}
|
||||
|
||||
serial, ok := soa["serial"].(float64)
|
||||
if !ok {
|
||||
return 0, errors.New("failed to retrieve SOA serial from zone data")
|
||||
}
|
||||
|
||||
newSerial := uint64(serial) + 1
|
||||
soa["serial"] = newSerial
|
||||
return newSerial, nil
|
||||
}
|
||||
|
||||
func deleteRecord(records []interface{}, name string) []interface{} {
|
||||
for pos := range records {
|
||||
if recordName, ok := records[pos].(map[string]interface{})["name"]; ok && recordName == name {
|
||||
return append(records[:pos], records[pos+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateRecord(records []interface{}, name string, record map[string]interface{}) []interface{} {
|
||||
for pos := range records {
|
||||
if records[pos].(map[string]interface{})["name"] == name {
|
||||
records[pos] = record
|
||||
return records
|
||||
}
|
||||
}
|
||||
|
||||
return append(records, record)
|
||||
return rec.Delete(zone)
|
||||
}
|
||||
|
||||
@@ -17,164 +17,395 @@ limitations under the License.
|
||||
package akamai
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
dns "github.com/akamai/AkamaiOPEN-edgegrid-golang/configdns-v2"
|
||||
|
||||
"github.com/jetstack/cert-manager/pkg/issuer/acme/dns/util"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const sampleZoneData = `{
|
||||
"token": "a184671d5307a388180fbf7f11dbdf46",
|
||||
"zone": {
|
||||
"name": "example.com",
|
||||
"soa": {
|
||||
"contact": "hostmaster.akamai.com.",
|
||||
"expire": 604800,
|
||||
"minimum": 180,
|
||||
"originserver": "use4.akamai.com.",
|
||||
"refresh": 900,
|
||||
"retry": 300,
|
||||
"serial": 1271354824,
|
||||
"ttl": 900
|
||||
},
|
||||
"ns": [
|
||||
{
|
||||
"active": true,
|
||||
"name": "",
|
||||
"target": "use4.akam.net.",
|
||||
"ttl": 3600
|
||||
},
|
||||
{
|
||||
"active": true,
|
||||
"name": "",
|
||||
"target": "use3.akam.net.",
|
||||
"ttl": 3600
|
||||
}
|
||||
]
|
||||
}
|
||||
}`
|
||||
func testRecordBodyData() *dns.RecordBody {
|
||||
|
||||
const sampleZoneDataWithTxt = `{
|
||||
"token": "a184671d5307a388180fbf7f11dbdf46",
|
||||
"zone": {
|
||||
"name": "example.com",
|
||||
"soa": {
|
||||
"contact": "hostmaster.akamai.com.",
|
||||
"expire": 604800,
|
||||
"minimum": 180,
|
||||
"originserver": "use4.akamai.com.",
|
||||
"refresh": 900,
|
||||
"retry": 300,
|
||||
"serial": 1271354825,
|
||||
"ttl": 900
|
||||
},
|
||||
"ns": [
|
||||
{
|
||||
"active": true,
|
||||
"name": "",
|
||||
"target": "use4.akam.net.",
|
||||
"ttl": 3600
|
||||
},
|
||||
{
|
||||
"active": true,
|
||||
"name": "",
|
||||
"target": "use3.akam.net.",
|
||||
"ttl": 3600
|
||||
}
|
||||
],
|
||||
"txt": [
|
||||
{
|
||||
"active": true,
|
||||
"name" :"_acme-challenge.test",
|
||||
"target": "dns01-key",
|
||||
"ttl": 60
|
||||
}
|
||||
]
|
||||
}
|
||||
}`
|
||||
|
||||
type httpResponder func(req *http.Request) (*http.Response, error)
|
||||
|
||||
func (r httpResponder) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return r(req)
|
||||
return &dns.RecordBody{
|
||||
Name: "_acme-challenge.test.example.com",
|
||||
RecordType: "TXT",
|
||||
Target: []string{`"` + "dns01-key" + `"`},
|
||||
TTL: 300,
|
||||
}
|
||||
}
|
||||
|
||||
func TestPresent(t *testing.T) {
|
||||
func testRecordBodyDataExist() *dns.RecordBody {
|
||||
|
||||
return &dns.RecordBody{
|
||||
Name: "_acme-challenge.test.example.com",
|
||||
RecordType: "TXT",
|
||||
Target: []string{`"` + "dns01-key" + `"`, `"` + "dns01-key-stub" + `"`},
|
||||
TTL: 300,
|
||||
}
|
||||
}
|
||||
|
||||
// OpenEdggrid DNS Stub
|
||||
type StubOpenDNSConfig struct {
|
||||
FuncOutput map[string]interface{}
|
||||
FuncErrors map[string]error
|
||||
}
|
||||
|
||||
//
|
||||
func findStubHostedDomainByFqdn(fqdn string, ns []string) (string, error) {
|
||||
|
||||
return "test.example.com", nil
|
||||
|
||||
}
|
||||
|
||||
func stubIsNotFoundTrue(err error) bool {
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func stubIsNotFoundFalse(err error) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// TestNewDNSProvider performs sanity check on provider init
|
||||
func TestNewDNSProvider(t *testing.T) {
|
||||
|
||||
akamai, err := NewDNSProvider("akamai.example.com", "token", "secret", "access-token", util.RecursiveNameservers)
|
||||
assert.NoError(t, err)
|
||||
// samplee couple important fields
|
||||
assert.Equal(t, akamai.serviceConsumerDomain, "akamai.example.com")
|
||||
assert.Equal(t, fmt.Sprintf("%T", akamai.dnsclient), "*akamai.OpenDNSConfig")
|
||||
|
||||
}
|
||||
|
||||
// TestPresentBasicFlow tests basic flow, e.g. no record exists.
|
||||
func TestPresentBasicFlow(t *testing.T) {
|
||||
akamai, err := NewDNSProvider("akamai.example.com", "token", "secret", "access-token", util.RecursiveNameservers)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var response []byte
|
||||
mockTransport(t, akamai, "example.com", sampleZoneData, &response)
|
||||
akamai.findHostedDomainByFqdn = findStubHostedDomainByFqdn
|
||||
akamai.isNotFound = stubIsNotFoundTrue
|
||||
akamai.dnsclient = &StubOpenDNSConfig{FuncOutput: map[string]interface{}{}, FuncErrors: map[string]error{}}
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["GetRecord"] = nil
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["RecordSave"] = testRecordBodyData()
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordUpdate"] = fmt.Errorf("Update not expected")
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordDelete"] = fmt.Errorf("Delete not expected")
|
||||
|
||||
assert.NoError(t, akamai.Present("test.example.com", "_acme-challenge.test.example.com.", "dns01-key"))
|
||||
|
||||
var expected, actual map[string]interface{}
|
||||
assert.NoError(t, json.Unmarshal([]byte(sampleZoneDataWithTxt), &expected))
|
||||
assert.NoError(t, json.Unmarshal(response, &actual))
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestCleanUp(t *testing.T) {
|
||||
// TestPresentExists tests flow with existing record.
|
||||
func TestPresentExists(t *testing.T) {
|
||||
akamai, err := NewDNSProvider("akamai.example.com", "token", "secret", "access-token", util.RecursiveNameservers)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var response []byte
|
||||
mockTransport(t, akamai, "example.com", sampleZoneDataWithTxt, &response)
|
||||
akamai.findHostedDomainByFqdn = findStubHostedDomainByFqdn
|
||||
akamai.isNotFound = stubIsNotFoundFalse // ignored for this flow ...
|
||||
akamai.dnsclient = &StubOpenDNSConfig{FuncOutput: map[string]interface{}{}, FuncErrors: map[string]error{}}
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["GetRecord"] = testRecordBodyData()
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["RecordUpdate"] = testRecordBodyDataExist()
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordSave"] = fmt.Errorf("Save not expected")
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordDelete"] = fmt.Errorf("Delete not expected")
|
||||
|
||||
assert.NoError(t, akamai.Present("test.example.com", "_acme-challenge.test.example.com.", "dns01-key-stub"))
|
||||
|
||||
}
|
||||
|
||||
// TestPresentValueExists tests flow with existing record.
|
||||
func TestPresentValueExists(t *testing.T) {
|
||||
akamai, err := NewDNSProvider("akamai.example.com", "token", "secret", "access-token", util.RecursiveNameservers)
|
||||
assert.NoError(t, err)
|
||||
|
||||
akamai.findHostedDomainByFqdn = findStubHostedDomainByFqdn
|
||||
akamai.isNotFound = stubIsNotFoundFalse // ignored for this flow ...
|
||||
akamai.dnsclient = &StubOpenDNSConfig{FuncOutput: map[string]interface{}{}, FuncErrors: map[string]error{}}
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["GetRecord"] = testRecordBodyData()
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordSave"] = fmt.Errorf("Save not expected")
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordUpdate"] = fmt.Errorf("Update not expected")
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordDelete"] = fmt.Errorf("Delete not expected")
|
||||
|
||||
assert.NoError(t, akamai.Present("test.example.com", "_acme-challenge.test.example.com.", "dns01-key"))
|
||||
|
||||
}
|
||||
|
||||
func TestPresentFailGetRecord(t *testing.T) {
|
||||
akamai, err := NewDNSProvider("akamai.example.com", "token", "secret", "access-token", util.RecursiveNameservers)
|
||||
assert.NoError(t, err)
|
||||
|
||||
akamai.findHostedDomainByFqdn = findStubHostedDomainByFqdn
|
||||
akamai.isNotFound = stubIsNotFoundFalse
|
||||
akamai.dnsclient = &StubOpenDNSConfig{FuncOutput: map[string]interface{}{}, FuncErrors: map[string]error{}}
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["GetRecord"] = nil
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["GetRecord"] = fmt.Errorf("Failed Get Test")
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordSave"] = fmt.Errorf("Save not expected")
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordUpdate"] = fmt.Errorf("Update not expected")
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordDelete"] = fmt.Errorf("Delete not expected")
|
||||
|
||||
assert.Error(t, akamai.Present("test.example.com", "_acme-challenge.test.example.com.", "dns01-key"))
|
||||
|
||||
}
|
||||
|
||||
func TestPresentFailSaveRecord(t *testing.T) {
|
||||
akamai, err := NewDNSProvider("akamai.example.com", "token", "secret", "access-token", util.RecursiveNameservers)
|
||||
assert.NoError(t, err)
|
||||
|
||||
akamai.findHostedDomainByFqdn = findStubHostedDomainByFqdn
|
||||
akamai.isNotFound = stubIsNotFoundTrue
|
||||
akamai.dnsclient = &StubOpenDNSConfig{FuncOutput: map[string]interface{}{}, FuncErrors: map[string]error{}}
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["GetRecord"] = nil
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordSave"] = fmt.Errorf("Save fail")
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordUpdate"] = fmt.Errorf("Update not expected")
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordDelete"] = fmt.Errorf("Delete not expected")
|
||||
|
||||
assert.Error(t, akamai.Present("test.example.com", "_acme-challenge.test.example.com.", "dns01-key"))
|
||||
|
||||
}
|
||||
|
||||
func TestPresentFailUpdateRecord(t *testing.T) {
|
||||
akamai, err := NewDNSProvider("akamai.example.com", "token", "secret", "access-token", util.RecursiveNameservers)
|
||||
assert.NoError(t, err)
|
||||
|
||||
akamai.findHostedDomainByFqdn = findStubHostedDomainByFqdn
|
||||
akamai.isNotFound = stubIsNotFoundFalse // ignored for this flow ...
|
||||
akamai.dnsclient = &StubOpenDNSConfig{FuncOutput: map[string]interface{}{}, FuncErrors: map[string]error{}}
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["GetRecord"] = testRecordBodyData()
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["RecordUpdate"] = testRecordBodyDataExist()
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordSave"] = fmt.Errorf("Save not expected")
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordUpdate"] = fmt.Errorf("Update failed")
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordDelete"] = fmt.Errorf("Delete not expected")
|
||||
|
||||
assert.Error(t, akamai.Present("test.example.com", "_acme-challenge.test.example.com.", "dns01-key-stub"))
|
||||
|
||||
}
|
||||
|
||||
// TestCleanUpBasicFlow tests flow with existing record.
|
||||
func TestCleanUpBasicFlow(t *testing.T) {
|
||||
akamai, err := NewDNSProvider("akamai.example.com", "token", "secret", "access-token", util.RecursiveNameservers)
|
||||
assert.NoError(t, err)
|
||||
|
||||
akamai.findHostedDomainByFqdn = findStubHostedDomainByFqdn
|
||||
akamai.isNotFound = stubIsNotFoundFalse // ignored for this flow ...
|
||||
akamai.dnsclient = &StubOpenDNSConfig{FuncOutput: map[string]interface{}{}, FuncErrors: map[string]error{}}
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["GetRecord"] = testRecordBodyData()
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["RecordDelete"] = testRecordBodyData()
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordSave"] = fmt.Errorf("Save not expected")
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordUpdate"] = fmt.Errorf("Update not expected")
|
||||
|
||||
assert.NoError(t, akamai.CleanUp("test.example.com", "_acme-challenge.test.example.com.", "dns01-key"))
|
||||
|
||||
var expected, actual map[string]interface{}
|
||||
assert.NoError(t, json.Unmarshal([]byte(sampleZoneData), &expected))
|
||||
expected["zone"].(map[string]interface{})["soa"].(map[string]interface{})["serial"] = 1271354826.
|
||||
assert.NoError(t, json.Unmarshal(response, &actual))
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
|
||||
func mockTransport(t *testing.T, akamai *DNSProvider, domain, data string, response *[]byte) {
|
||||
akamai.transport = httpResponder(func(req *http.Request) (*http.Response, error) {
|
||||
defer req.Body.Close()
|
||||
// TestPresentExists tests flow with existing record.
|
||||
func TestCleanUpExists(t *testing.T) {
|
||||
akamai, err := NewDNSProvider("akamai.example.com", "token", "secret", "access-token", util.RecursiveNameservers)
|
||||
assert.NoError(t, err)
|
||||
|
||||
if req.URL.String() != "https://akamai.example.com/config-dns/v1/zones/"+domain {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Body: http.NoBody,
|
||||
}, nil
|
||||
}
|
||||
akamai.findHostedDomainByFqdn = findStubHostedDomainByFqdn
|
||||
akamai.isNotFound = stubIsNotFoundFalse // ignored for this flow ...
|
||||
akamai.dnsclient = &StubOpenDNSConfig{FuncOutput: map[string]interface{}{}, FuncErrors: map[string]error{}}
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["GetRecord"] = testRecordBodyData()
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["RecordUpdate"] = testRecordBodyDataExist()
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordSave"] = fmt.Errorf("Save not expected")
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordDelete"] = fmt.Errorf("Delete not expected")
|
||||
|
||||
if req.Method == http.MethodGet {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte(data))),
|
||||
}, nil
|
||||
}
|
||||
assert.NoError(t, akamai.CleanUp("test.example.com", "_acme-challenge.test.example.com.", "dns01-key-stub"))
|
||||
|
||||
if req.Method == http.MethodPost {
|
||||
if req.Header.Get("Content-Type") != "application/json" {
|
||||
t.Fatalf("unsupported Content Type: %v", req.Header.Get("Content-Type"))
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
*response, err = ioutil.ReadAll(req.Body)
|
||||
assert.NoError(t, err)
|
||||
// TestCleanUpExistsNoValue tests flow with existing record.
|
||||
func TestCleanUpExistsNoValue(t *testing.T) {
|
||||
akamai, err := NewDNSProvider("akamai.example.com", "token", "secret", "access-token", util.RecursiveNameservers)
|
||||
assert.NoError(t, err)
|
||||
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusNoContent,
|
||||
Body: http.NoBody,
|
||||
}, nil
|
||||
}
|
||||
akamai.findHostedDomainByFqdn = findStubHostedDomainByFqdn
|
||||
akamai.isNotFound = stubIsNotFoundFalse // ignored for this flow ...
|
||||
akamai.dnsclient = &StubOpenDNSConfig{FuncOutput: map[string]interface{}{}, FuncErrors: map[string]error{}}
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["GetRecord"] = testRecordBodyData()
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordSave"] = fmt.Errorf("Save not expected")
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordUpdate"] = fmt.Errorf("Update not expected")
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordDelete"] = fmt.Errorf("Delete not expected")
|
||||
|
||||
t.Fatalf("unexpected method: %v", req.Method)
|
||||
return nil, nil
|
||||
})
|
||||
akamai.findHostedDomainByFqdn = func(fqdn string, _ []string) (string, error) {
|
||||
if !strings.HasSuffix(fqdn, domain+".") {
|
||||
t.Fatalf("unexpected fqdn: %s", fqdn)
|
||||
}
|
||||
return domain, nil
|
||||
assert.NoError(t, akamai.CleanUp("test.example.com", "_acme-challenge.test.example.com.", "dns01-key-stub"))
|
||||
|
||||
}
|
||||
|
||||
// TestCleanUpNoRecord tests flow with no existing record.
|
||||
func TestCleanUpNoRecord(t *testing.T) {
|
||||
akamai, err := NewDNSProvider("akamai.example.com", "token", "secret", "access-token", util.RecursiveNameservers)
|
||||
assert.NoError(t, err)
|
||||
|
||||
akamai.findHostedDomainByFqdn = findStubHostedDomainByFqdn
|
||||
akamai.isNotFound = stubIsNotFoundTrue // ignored for this flow ...
|
||||
akamai.dnsclient = &StubOpenDNSConfig{FuncOutput: map[string]interface{}{}, FuncErrors: map[string]error{}}
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["GetRecord"] = nil
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordSave"] = fmt.Errorf("Save not expected")
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordUpdate"] = fmt.Errorf("Update not expected")
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordDelete"] = fmt.Errorf("Delete not expected")
|
||||
|
||||
assert.NoError(t, akamai.CleanUp("test.example.com", "_acme-challenge.test.example.com.", "dns01"))
|
||||
|
||||
}
|
||||
|
||||
func TestCleanUpFailGetRecord(t *testing.T) {
|
||||
akamai, err := NewDNSProvider("akamai.example.com", "token", "secret", "access-token", util.RecursiveNameservers)
|
||||
assert.NoError(t, err)
|
||||
|
||||
akamai.findHostedDomainByFqdn = findStubHostedDomainByFqdn
|
||||
akamai.isNotFound = stubIsNotFoundFalse
|
||||
akamai.dnsclient = &StubOpenDNSConfig{FuncOutput: map[string]interface{}{}, FuncErrors: map[string]error{}}
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["GetRecord"] = nil
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["GetRecord"] = fmt.Errorf("Failed Get Record")
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordSave"] = fmt.Errorf("Save not expected")
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordUpdate"] = fmt.Errorf("Update not expected")
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordDelete"] = fmt.Errorf("Delete not expected")
|
||||
|
||||
assert.Error(t, akamai.CleanUp("test.example.com", "_acme-challenge.test.example.com.", "dns01-key"))
|
||||
|
||||
}
|
||||
|
||||
func TestCleanUpFailUpdateRecord(t *testing.T) {
|
||||
akamai, err := NewDNSProvider("akamai.example.com", "token", "secret", "access-token", util.RecursiveNameservers)
|
||||
assert.NoError(t, err)
|
||||
|
||||
akamai.findHostedDomainByFqdn = findStubHostedDomainByFqdn
|
||||
akamai.isNotFound = stubIsNotFoundFalse // ignored for this flow ...
|
||||
akamai.dnsclient = &StubOpenDNSConfig{FuncOutput: map[string]interface{}{}, FuncErrors: map[string]error{}}
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["GetRecord"] = testRecordBodyDataExist()
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["RecordUpdate"] = testRecordBodyData()
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordSave"] = fmt.Errorf("Save not expected")
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordUpdate"] = fmt.Errorf("Update failed")
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordDelete"] = fmt.Errorf("Delete not expected")
|
||||
|
||||
assert.Error(t, akamai.CleanUp("test.example.com", "_acme-challenge.test.example.com.", "dns01-key-stub"))
|
||||
|
||||
}
|
||||
|
||||
func TestCleanUpFailDeleteRecord(t *testing.T) {
|
||||
akamai, err := NewDNSProvider("akamai.example.com", "token", "secret", "access-token", util.RecursiveNameservers)
|
||||
assert.NoError(t, err)
|
||||
|
||||
akamai.findHostedDomainByFqdn = findStubHostedDomainByFqdn
|
||||
akamai.isNotFound = stubIsNotFoundFalse // ignored for this flow ...
|
||||
akamai.dnsclient = &StubOpenDNSConfig{FuncOutput: map[string]interface{}{}, FuncErrors: map[string]error{}}
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["GetRecord"] = testRecordBodyData()
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["RecordDelete"] = testRecordBodyData()
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordSave"] = fmt.Errorf("Save not expected")
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordUpdate"] = fmt.Errorf("Update not expected")
|
||||
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordDelete"] = fmt.Errorf("Delete failed")
|
||||
|
||||
assert.Error(t, akamai.CleanUp("test.example.com", "_acme-challenge.test.example.com.", "dns01-key"))
|
||||
|
||||
}
|
||||
|
||||
// Stub Get Record
|
||||
func (o StubOpenDNSConfig) GetRecord(zone string, name string, record_type string) (*dns.RecordBody, error) {
|
||||
|
||||
var rec *dns.RecordBody
|
||||
|
||||
err, ok := o.FuncErrors["GetRecord"]
|
||||
if ok {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
exp, ok := o.FuncOutput["GetRecord"]
|
||||
if ok {
|
||||
if exp == nil {
|
||||
return nil, fmt.Errorf("GetRecord: Unexpected nil")
|
||||
}
|
||||
rec = exp.(*dns.RecordBody)
|
||||
// comare passed with expected
|
||||
if name != rec.Name {
|
||||
return nil, fmt.Errorf("GetRecord: expected/actual Name don't match")
|
||||
}
|
||||
if record_type != rec.RecordType {
|
||||
return nil, fmt.Errorf("GetRecord: expected/actual Record Type don't match")
|
||||
}
|
||||
}
|
||||
|
||||
return rec, nil
|
||||
|
||||
}
|
||||
|
||||
func (o StubOpenDNSConfig) RecordSave(rec *dns.RecordBody, zone string) error {
|
||||
|
||||
exp, ok := o.FuncOutput["RecordSave"]
|
||||
if ok {
|
||||
// comare passed with expected
|
||||
if rec.Name != exp.(*dns.RecordBody).Name {
|
||||
return fmt.Errorf("RecordSave: expected/actual Name don't match")
|
||||
}
|
||||
if rec.RecordType != exp.(*dns.RecordBody).RecordType {
|
||||
return fmt.Errorf("RecordSave: expected/actual Record Type don't match")
|
||||
}
|
||||
if !reflect.DeepEqual(rec.Target, exp.(*dns.RecordBody).Target) {
|
||||
return fmt.Errorf("RecordSave: expected/actual Target don't match")
|
||||
}
|
||||
if rec.TTL != exp.(*dns.RecordBody).TTL {
|
||||
return fmt.Errorf("RecordSave: expected/actual TTL don't match")
|
||||
}
|
||||
}
|
||||
err, ok := o.FuncErrors["RecordSave"]
|
||||
if ok {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (o StubOpenDNSConfig) RecordUpdate(rec *dns.RecordBody, zone string) error {
|
||||
|
||||
exp, ok := o.FuncOutput["RecordUpdate"]
|
||||
if ok {
|
||||
// comare passed with expected
|
||||
if rec.Name != exp.(*dns.RecordBody).Name {
|
||||
return fmt.Errorf("RecordUpdate: expected/actual Name don't match")
|
||||
}
|
||||
if rec.RecordType != exp.(*dns.RecordBody).RecordType {
|
||||
return fmt.Errorf("RecordUpdate: expected/actual Record Type don't match")
|
||||
}
|
||||
if !reflect.DeepEqual(rec.Target, exp.(*dns.RecordBody).Target) {
|
||||
return fmt.Errorf("RecordUpdate: expected/actual Target don't match")
|
||||
}
|
||||
if rec.TTL != exp.(*dns.RecordBody).TTL {
|
||||
return fmt.Errorf("RecordUpdate: expected/actual TTL don't match")
|
||||
}
|
||||
}
|
||||
err, ok := o.FuncErrors["RecordUpdate"]
|
||||
if ok {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o StubOpenDNSConfig) RecordDelete(rec *dns.RecordBody, zone string) error {
|
||||
|
||||
exp, ok := o.FuncOutput["RecordDelete"]
|
||||
if ok {
|
||||
// comare passed with expected
|
||||
if rec.Name != exp.(*dns.RecordBody).Name {
|
||||
return fmt.Errorf("RecordDelete: expected/actual Name don't match")
|
||||
}
|
||||
if rec.RecordType != exp.(*dns.RecordBody).RecordType {
|
||||
return fmt.Errorf("RecordDelete: expected/actual Record Type don't match")
|
||||
}
|
||||
if !reflect.DeepEqual(rec.Target, exp.(*dns.RecordBody).Target) {
|
||||
return fmt.Errorf("RecordDelete: expected/actual Target don't match")
|
||||
}
|
||||
if rec.TTL != exp.(*dns.RecordBody).TTL {
|
||||
return fmt.Errorf("RecordDelete: expected/actual TTL don't match")
|
||||
}
|
||||
}
|
||||
err, ok := o.FuncErrors["RecordDelete"]
|
||||
if ok {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,280 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 The cert-manager Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package akamai
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// EdgeGridAuth holds all values required to perform Akamai API Client Authentication.
|
||||
// See https://developer.akamai.com/introduction/Client_Auth.html.
|
||||
type EdgeGridAuth struct {
|
||||
ClientToken string
|
||||
ClientSecret string
|
||||
AccessToken string
|
||||
HeadersToSign []string
|
||||
MaxBody int
|
||||
|
||||
now func() time.Time
|
||||
createNonce func() (string, error)
|
||||
}
|
||||
|
||||
type signingData struct {
|
||||
timestamp string
|
||||
authHeader string
|
||||
dataToSign string
|
||||
}
|
||||
|
||||
// edgeGridAuthTimeFormat is used for timestamps in request signatures.
|
||||
const edgeGridAuthTimeFormat = "20060102T15:04:05-0700" // yyyyMMddTHH:mm:ss+0000
|
||||
|
||||
const NoMaxBody = -1
|
||||
|
||||
// NewEdgeGridAuth returns a new request signer for Akamai EdgeGrid
|
||||
func NewEdgeGridAuth(clientToken, clientSecret, accessToken string, headersToSign ...string) *EdgeGridAuth {
|
||||
return &EdgeGridAuth{
|
||||
ClientToken: clientToken,
|
||||
ClientSecret: clientSecret,
|
||||
AccessToken: accessToken,
|
||||
HeadersToSign: headersToSign,
|
||||
MaxBody: NoMaxBody,
|
||||
|
||||
now: time.Now,
|
||||
createNonce: createRandomNonce,
|
||||
}
|
||||
}
|
||||
|
||||
// SignRequest calculates the signature for Akamai Open API and adds it as the Authorization header.
|
||||
// The Authorization header starts with the signing algorithm moniker (name of the algorithm) used to sign the request.
|
||||
// The moniker below identifies EdgeGrid V1, hash message authentication code, SHA–256 as the hash standard.
|
||||
// This moniker is then followed by a space and an ordered list of name value pairs with each field separated by a semicolon.
|
||||
func (e *EdgeGridAuth) SignRequest(req *http.Request) error {
|
||||
signingData, err := e.signingData(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", fmt.Sprintf(
|
||||
"%ssignature=%s",
|
||||
signingData.authHeader,
|
||||
e.calculateRequestSignature(signingData)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *EdgeGridAuth) calculateRequestSignature(signingData *signingData) string {
|
||||
return computeSignature(
|
||||
signingData.dataToSign,
|
||||
e.signingKey(signingData.timestamp))
|
||||
}
|
||||
|
||||
func (e *EdgeGridAuth) signingData(req *http.Request) (*signingData, error) {
|
||||
nonce, err := e.createNonce()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
timestamp := e.now().UTC().Format(edgeGridAuthTimeFormat)
|
||||
authHeader := fmt.Sprintf("EG1-HMAC-SHA256 client_token=%s;access_token=%s;timestamp=%s;nonce=%s;",
|
||||
e.ClientToken,
|
||||
e.AccessToken,
|
||||
timestamp,
|
||||
nonce)
|
||||
|
||||
return &signingData{
|
||||
timestamp: timestamp,
|
||||
authHeader: authHeader,
|
||||
dataToSign: e.dataToSign(req, authHeader),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// dataToSign includes the information from the HTTP request that is relevant to ensuring that the request is authentic.
|
||||
// This data set comprised of the request data combined with the authorization header value (excluding the signature field,
|
||||
// but including the ; right before the signature field).
|
||||
func (e *EdgeGridAuth) dataToSign(req *http.Request, authHeader string) string {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
buffer.WriteString(req.Method)
|
||||
buffer.WriteRune('\t')
|
||||
buffer.WriteString(req.URL.Scheme)
|
||||
buffer.WriteRune('\t')
|
||||
buffer.WriteString(req.URL.Host)
|
||||
buffer.WriteRune('\t')
|
||||
buffer.WriteString(relativeURL(req.URL))
|
||||
buffer.WriteRune('\t')
|
||||
buffer.WriteString(e.canonicalizedHeaders(req))
|
||||
buffer.WriteRune('\t')
|
||||
buffer.WriteString(e.computeBodyHash(req))
|
||||
buffer.WriteRune('\t')
|
||||
buffer.WriteString(authHeader)
|
||||
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
// signingKey is derived from the client secret.
|
||||
// The signing key is computed as the base64 encoding of the SHA–256 HMAC of the timestamp string
|
||||
// (the field value included in the HTTP authorization header described above) with the client secret as the key.
|
||||
func (e *EdgeGridAuth) signingKey(timestamp string) string {
|
||||
return computeSignature(timestamp, e.ClientSecret)
|
||||
}
|
||||
|
||||
// relativeURL is the part of the URL that starts from the root path and includes the query string, with the handling of following special cases:
|
||||
// If the path is null or empty, set it to / (forward-slash).
|
||||
// If the path does not start with /, add / to the beginning.
|
||||
func relativeURL(url *url.URL) string {
|
||||
relativeURL := url.Path
|
||||
if relativeURL == "" {
|
||||
return "/"
|
||||
}
|
||||
|
||||
if relativeURL[0] != '/' {
|
||||
relativeURL = "/" + relativeURL
|
||||
}
|
||||
|
||||
if url.RawQuery != "" {
|
||||
relativeURL += "?"
|
||||
relativeURL += url.RawQuery
|
||||
}
|
||||
|
||||
return relativeURL
|
||||
}
|
||||
|
||||
// computeBodyHash returns the base64-encoded SHA–256 hash of the POST body.
|
||||
// For any other request methods, this field is empty. But the tac separator (\t) must be included.
|
||||
// The size of the POST body must be less than or equal to the value specified by the service.
|
||||
// Any request that does not meet this criteria SHOULD be rejected during the signing process,
|
||||
// as the request will be rejected by EdgeGrid.
|
||||
func (e *EdgeGridAuth) computeBodyHash(req *http.Request) string {
|
||||
if req.Body != nil {
|
||||
bodyBytes, _ := ioutil.ReadAll(req.Body)
|
||||
req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
|
||||
if req.Method == http.MethodPost && len(bodyBytes) > 0 {
|
||||
dataToHash := bodyBytes
|
||||
if e.MaxBody != NoMaxBody && len(dataToHash) > e.MaxBody {
|
||||
dataToHash = dataToHash[0:e.MaxBody]
|
||||
}
|
||||
sha256Sum := sha256.Sum256(dataToHash)
|
||||
return base64.StdEncoding.EncodeToString(sha256Sum[:])
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// canonicalizedHeaders returns the request headers as a canonicalized string.
|
||||
//
|
||||
// The protocol does not support multiple request headers with the same header name.
|
||||
// Such requests SHOULD be rejected during the signing process. Otherwise, EdgeGrid
|
||||
// will not produce the intended results by rejecting such requests or removing all
|
||||
// (but one) duplicated headers.
|
||||
//
|
||||
// Header names are case-insensitive per rfc2616.
|
||||
//
|
||||
// For each entry in the list of headers designated by the service provider to include
|
||||
// in the signature in the specified order, the canonicalization of the request header
|
||||
// is done as follows:
|
||||
//
|
||||
// Get the first header value for the name.
|
||||
// Trim the leading and trailing white spaces.
|
||||
// Replace all repeated white spaces with a single space.
|
||||
// Concatenate the name:value pairs with the tab (\t) separator (name field is all in lower case).
|
||||
// Terminate the headers with another tab (\t) separator.
|
||||
//
|
||||
// NOTE: The canonicalized data is used for creating the signature only, as this step
|
||||
// might alter the header value. If a header in the list is not present in the request,
|
||||
// or the header value is empty, nothing for that header, neither the name nor the tab
|
||||
// separator, may be included.
|
||||
func (e *EdgeGridAuth) canonicalizedHeaders(req *http.Request) string {
|
||||
if len(e.HeadersToSign) < 1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var headerNamesToSign []string
|
||||
for headerName := range req.Header {
|
||||
for _, sign := range e.HeadersToSign {
|
||||
if strings.EqualFold(sign, headerName) {
|
||||
headerNamesToSign = append(headerNamesToSign, headerName)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(headerNamesToSign) < 1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
sort.Strings(headerNamesToSign)
|
||||
|
||||
var buffer bytes.Buffer
|
||||
for _, headerName := range headerNamesToSign {
|
||||
for _, c := range headerName {
|
||||
buffer.WriteRune(unicode.ToLower(c))
|
||||
}
|
||||
|
||||
buffer.WriteRune(':')
|
||||
|
||||
white := false
|
||||
empty := true
|
||||
for _, c := range req.Header.Get(headerName) {
|
||||
if unicode.IsSpace(c) {
|
||||
white = true
|
||||
} else {
|
||||
if white && !empty {
|
||||
buffer.WriteRune(' ')
|
||||
}
|
||||
buffer.WriteRune(unicode.ToLower(c))
|
||||
empty = false
|
||||
white = false
|
||||
}
|
||||
}
|
||||
|
||||
buffer.WriteRune('\t')
|
||||
}
|
||||
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
// calculateSignature is the base64-encoding of the SHA–256 HMAC of the data to sign with the signing key.
|
||||
func computeSignature(message string, secret string) string {
|
||||
key := []byte(secret)
|
||||
h := hmac.New(sha256.New, key)
|
||||
h.Write([]byte(message))
|
||||
return base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
func createRandomNonce() (string, error) {
|
||||
bytes := make([]byte, 18)
|
||||
_, err := rand.Read(bytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return base64.URLEncoding.EncodeToString(bytes), nil
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 The cert-manager Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package akamai
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDataToSign(t *testing.T) {
|
||||
req, err := http.NewRequest(
|
||||
http.MethodGet,
|
||||
"https://akaa-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net/diagnostic-tools/v1/locations",
|
||||
http.NoBody)
|
||||
assert.NoError(t, err)
|
||||
|
||||
auth := NewEdgeGridAuth("ClientToken", "ClientSecret", "AccessToken")
|
||||
auth.now = func() time.Time {
|
||||
return time.Unix(1396461906, 0) // 20140402T18:05:06Z
|
||||
}
|
||||
auth.createNonce = func() (string, error) {
|
||||
return "185f94eb-537c-4c01-b8cc-2fa5a06aee7f", nil
|
||||
}
|
||||
|
||||
data, err := auth.signingData(req)
|
||||
assert.NoError(t, err)
|
||||
|
||||
expected := "GET" +
|
||||
"\thttps" +
|
||||
"\takaa-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net" +
|
||||
"\t/diagnostic-tools/v1/locations" +
|
||||
"\t" + // headers
|
||||
"\t" + // content hash
|
||||
"\tEG1-HMAC-SHA256 " +
|
||||
"client_token=ClientToken;" +
|
||||
"access_token=AccessToken;" +
|
||||
"timestamp=20140402T18:05:06+0000;" +
|
||||
"nonce=185f94eb-537c-4c01-b8cc-2fa5a06aee7f;"
|
||||
|
||||
assert.EqualValues(t, expected, data.dataToSign)
|
||||
}
|
||||
|
||||
func TestDataToSignWithHeaders(t *testing.T) {
|
||||
req, err := http.NewRequest(
|
||||
http.MethodGet,
|
||||
"http://akaa-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna-dev.akamaiapis.net/sample-api/v1/property/?fields=x&format=json&cpcode=1234",
|
||||
http.NoBody)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req.Header.Set("x-a", "va")
|
||||
req.Header.Set("x-c", "\" xc \"")
|
||||
req.Header.Set("x-b", " w b")
|
||||
|
||||
auth := NewEdgeGridAuth(
|
||||
"ClientToken", "ClientSecret", "AccessToken",
|
||||
"x-c", "x-b", "x-a")
|
||||
auth.now = func() time.Time {
|
||||
return time.Unix(1376917283, 0) // 20130819T13:01:23Z
|
||||
}
|
||||
auth.createNonce = func() (string, error) {
|
||||
return "ac392096-8aa1-44fd-8c3b-f797d35a6736", nil
|
||||
}
|
||||
|
||||
data, err := auth.signingData(req)
|
||||
assert.NoError(t, err)
|
||||
|
||||
expected := "GET" +
|
||||
"\thttp" +
|
||||
"\takaa-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna-dev.akamaiapis.net" +
|
||||
"\t/sample-api/v1/property/?fields=x&format=json&cpcode=1234" +
|
||||
"\tx-a:va\tx-b:w b\tx-c:\" xc \"\t" +
|
||||
"\t" + // content hash
|
||||
"\tEG1-HMAC-SHA256 " +
|
||||
"client_token=ClientToken;" +
|
||||
"access_token=AccessToken;" +
|
||||
"timestamp=20130819T13:01:23+0000;" +
|
||||
"nonce=ac392096-8aa1-44fd-8c3b-f797d35a6736;"
|
||||
|
||||
assert.EqualValues(t, expected, data.dataToSign)
|
||||
}
|
||||
Reference in New Issue
Block a user