1
0
Fork 0

Fix: Add TTL and custom Timeout in DigitalOcean DNS provider

This commit is contained in:
Ludovic Fernandez 2018-04-06 17:04:03 +02:00 committed by Traefiker Bot
parent 66485e81b4
commit 0ef1b7b683
120 changed files with 23764 additions and 9782 deletions

View file

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
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.

View file

@ -0,0 +1,36 @@
package client
import (
"encoding/json"
)
// Resource is the "base" type for all API resources
type Resource struct {
Complete chan bool `json:"-"`
}
// Init initializes the Complete channel, if it is necessary
// need to create a resource specific Init(), make sure to
// initialize the channel.
func (resource *Resource) Init() {
resource.Complete = make(chan bool, 1)
}
// PostUnmarshalJSON is a default implementation of the
// PostUnmarshalJSON hook that simply calls Init() and
// sends true to the Complete channel. This is overridden
// in many resources, in particular those that represent
// collections, and have to initialize sub-resources also.
func (resource *Resource) PostUnmarshalJSON() error {
resource.Init()
resource.Complete <- true
return nil
}
// GetJSON returns the raw (indented) JSON (as []bytes)
func (resource *Resource) GetJSON() ([]byte, error) {
return json.MarshalIndent(resource, "", " ")
}
// JSONBody is a generic struct for temporary JSON unmarshalling.
type JSONBody map[string]interface{}

View file

@ -0,0 +1,111 @@
// Package client is a simple library for http.Client to sign Akamai OPEN Edgegrid API requests
package client
import (
"bytes"
"errors"
"io"
"io/ioutil"
"net/http"
"net/url"
"runtime"
"strings"
"github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid"
"github.com/akamai/AkamaiOPEN-edgegrid-golang/jsonhooks-v1"
)
var (
libraryVersion = "0.6.0"
// UserAgent is the User-Agent value sent for all requests
UserAgent = "Akamai-Open-Edgegrid-golang/" + libraryVersion + " golang/" + strings.TrimPrefix(runtime.Version(), "go")
// Client is the *http.Client to use
Client = http.DefaultClient
)
// NewRequest creates an HTTP request that can be sent to Akamai APIs. A relative URL can be provided in path, which will be resolved to the
// Host specified in Config. If body is specified, it will be sent as the request body.
func NewRequest(config edgegrid.Config, method, path string, body io.Reader) (*http.Request, error) {
var (
baseURL *url.URL
err error
)
if strings.HasPrefix(config.Host, "https://") {
baseURL, err = url.Parse(config.Host)
} else {
baseURL, err = url.Parse("https://" + config.Host)
}
if err != nil {
return nil, err
}
rel, err := url.Parse(strings.TrimPrefix(path, "/"))
if err != nil {
return nil, err
}
u := baseURL.ResolveReference(rel)
req, err := http.NewRequest(method, u.String(), body)
if err != nil {
return nil, err
}
req.Header.Add("User-Agent", UserAgent)
return req, nil
}
// NewJSONRequest creates an HTTP request that can be sent to the Akamai APIs with a JSON body
// The JSON body is encoded and the Content-Type/Accept headers are set automatically.
func NewJSONRequest(config edgegrid.Config, method, path string, body interface{}) (*http.Request, error) {
jsonBody, err := jsonhooks.Marshal(body)
if err != nil {
return nil, err
}
buf := bytes.NewReader(jsonBody)
req, err := NewRequest(config, method, path, buf)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json,*/*")
return req, nil
}
// Do performs a given HTTP Request, signed with the Akamai OPEN Edgegrid
// Authorization header. An edgegrid.Response or an error is returned.
func Do(config edgegrid.Config, req *http.Request) (*http.Response, error) {
Client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
req = edgegrid.AddRequestHeader(config, req)
return nil
}
req = edgegrid.AddRequestHeader(config, req)
res, err := Client.Do(req)
if err != nil {
return nil, err
}
return res, nil
}
// BodyJSON unmarshals the Response.Body into a given data structure
func BodyJSON(r *http.Response, data interface{}) error {
if data == nil {
return errors.New("You must pass in an interface{}")
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
return err
}
err = jsonhooks.Unmarshal(body, data)
return err
}

View file

@ -0,0 +1,88 @@
package client
import (
"fmt"
"io/ioutil"
"net/http"
"strings"
"github.com/akamai/AkamaiOPEN-edgegrid-golang/jsonhooks-v1"
)
// APIError exposes an Akamai OPEN Edgegrid Error
type APIError struct {
error
Type string `json:"type"`
Title string `json:"title"`
Status int `json:"status"`
Detail string `json:"detail"`
Instance string `json:"instance"`
Method string `json:"method"`
ServerIP string `json:"serverIp"`
ClientIP string `json:"clientIp"`
RequestID string `json:"requestId"`
RequestTime string `json:"requestTime"`
Response *http.Response `json:"-"`
RawBody string `json:"-"`
}
func (error APIError) Error() string {
return strings.TrimSpace(fmt.Sprintf("API Error: %d %s %s More Info %s", error.Status, error.Title, error.Detail, error.Type))
}
// NewAPIError creates a new API error based on a Response,
// or http.Response-like.
func NewAPIError(response *http.Response) APIError {
// TODO: handle this error
body, _ := ioutil.ReadAll(response.Body)
return NewAPIErrorFromBody(response, body)
}
// NewAPIErrorFromBody creates a new API error, allowing you to pass in a body
//
// This function is intended to be used after the body has already been read for
// other purposes.
func NewAPIErrorFromBody(response *http.Response, body []byte) APIError {
error := APIError{}
if err := jsonhooks.Unmarshal(body, &error); err == nil {
error.Status = response.StatusCode
error.Title = response.Status
}
error.Response = response
error.RawBody = string(body)
return error
}
// IsInformational determines if a response was informational (1XX status)
func IsInformational(r *http.Response) bool {
return r.StatusCode > 99 && r.StatusCode < 200
}
// IsSuccess determines if a response was successful (2XX status)
func IsSuccess(r *http.Response) bool {
return r.StatusCode > 199 && r.StatusCode < 300
}
// IsRedirection determines if a response was a redirect (3XX status)
func IsRedirection(r *http.Response) bool {
return r.StatusCode > 299 && r.StatusCode < 400
}
// IsClientError determines if a response was a client error (4XX status)
func IsClientError(r *http.Response) bool {
return r.StatusCode > 399 && r.StatusCode < 500
}
// IsServerError determines if a response was a server error (5XX status)
func IsServerError(r *http.Response) bool {
return r.StatusCode > 499 && r.StatusCode < 600
}
// IsError determines if the response was a client or server error (4XX or 5XX status)
func IsError(r *http.Response) bool {
return r.StatusCode > 399 && r.StatusCode < 600
}

View file

@ -0,0 +1,125 @@
package dns
import (
"fmt"
)
type ConfigDNSError interface {
error
Network() bool
NotFound() bool
FailedToSave() bool
ValidationFailed() bool
}
func IsConfigDNSError(e error) bool {
_, ok := e.(ConfigDNSError)
return ok
}
type ZoneError struct {
zoneName string
httpErrorMessage string
apiErrorMessage string
err error
}
func (e *ZoneError) Network() bool {
if e.httpErrorMessage != "" {
return true
}
return false
}
func (e *ZoneError) NotFound() bool {
if e.err == nil && e.httpErrorMessage == "" && e.apiErrorMessage == "" {
return true
}
return false
}
func (e *ZoneError) FailedToSave() bool {
return false
}
func (e *ZoneError) ValidationFailed() bool {
if e.apiErrorMessage != "" {
return true
}
return false
}
func (e *ZoneError) Error() string {
if e.Network() {
return fmt.Sprintf("Zone \"%s\" network error: [%s]", e.zoneName, e.httpErrorMessage)
}
if e.NotFound() {
return fmt.Sprintf("Zone \"%s\" not found.", e.zoneName)
}
if e.FailedToSave() {
return fmt.Sprintf("Zone \"%s\" failed to save: [%s]", e.zoneName, e.err.Error())
}
if e.ValidationFailed() {
return fmt.Sprintf("Zone \"%s\" validation failed: [%s]", e.zoneName, e.apiErrorMessage)
}
if e.err != nil {
return e.err.Error()
}
return "<nil>"
}
type RecordError struct {
fieldName string
httpErrorMessage string
err error
}
func (e *RecordError) Network() bool {
if e.httpErrorMessage != "" {
return true
}
return false
}
func (e *RecordError) NotFound() bool {
return false
}
func (e *RecordError) FailedToSave() bool {
if e.fieldName == "" {
return true
}
return false
}
func (e *RecordError) ValidationFailed() bool {
if e.fieldName != "" {
return true
}
return false
}
func (e *RecordError) Error() string {
if e.Network() {
return fmt.Sprintf("Record network error: [%s]", e.httpErrorMessage)
}
if e.NotFound() {
return fmt.Sprintf("Record not found.")
}
if e.FailedToSave() {
return fmt.Sprintf("Record failed to save: [%s]", e.err.Error())
}
if e.ValidationFailed() {
return fmt.Sprintf("Record validation failed for field [%s]", e.fieldName)
}
return "<nil>"
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,16 @@
package dns
import (
"github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid"
)
var (
// Config contains the Akamai OPEN Edgegrid API credentials
// for automatic signing of requests
Config edgegrid.Config
)
// Init sets the FastDNS edgegrid Config
func Init(config edgegrid.Config) {
Config = config
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,181 @@
package edgegrid
import (
"fmt"
"os"
"strconv"
"strings"
"github.com/go-ini/ini"
"gopkg.in/mattes/go-expand-tilde.v1"
)
// Config struct provides all the necessary fields to
// create authorization header, debug is optional
type Config struct {
Host string `ini:"host"`
ClientToken string `ini:"client_token"`
ClientSecret string `ini:"client_secret"`
AccessToken string `ini:"access_token"`
HeaderToSign []string `ini:"headers_to_sign"`
MaxBody int `ini:"max_body"`
Debug bool `ini:"debug"`
}
// Init initializes by first attempting to use ENV vars, with .edgerc as a fallback
//
// See: InitEnv()
// See: InitEdgeRc()
func Init(filepath string, section string) (Config, error) {
if section == "" {
section = defaultSection
} else {
section = strings.ToUpper(section)
}
_, exists := os.LookupEnv("AKAMAI_" + section + "_HOST")
if !exists && section == defaultSection {
_, exists := os.LookupEnv("AKAMAI_HOST")
if exists {
return InitEnv("")
}
}
if exists {
return InitEnv(section)
}
c, err := InitEdgeRc(filepath, strings.ToLower(section))
if err == nil {
return c, nil
}
if section != defaultSection {
_, ok := os.LookupEnv("AKAMAI_HOST")
if ok {
return InitEnv("")
}
}
return c, fmt.Errorf("Unable to create instance using environment or .edgerc file")
}
// InitEdgeRc initializes using a configuration file in standard INI format
//
// By default, it uses the .edgerc found in the users home directory, and the
// "default" section.
func InitEdgeRc(filepath string, section string) (Config, error) {
var (
c Config
requiredOptions = []string{"host", "client_token", "client_secret", "access_token"}
missing []string
)
// Check if filepath is empty
if filepath == "" {
filepath = "~/.edgerc"
}
// Check if section is empty
if section == "" {
section = "default"
}
// Tilde seems to be not working when passing ~/.edgerc as file
// Takes current user and use home dir instead
path, err := tilde.Expand(filepath)
if err != nil {
return c, fmt.Errorf(errorMap[ErrHomeDirNotFound], err)
}
edgerc, err := ini.Load(path)
if err != nil {
return c, fmt.Errorf(errorMap[ErrConfigFile], err)
}
err = edgerc.Section(section).MapTo(&c)
if err != nil {
return c, fmt.Errorf(errorMap[ErrConfigFileSection], err)
}
for _, opt := range requiredOptions {
if !(edgerc.Section(section).HasKey(opt)) {
missing = append(missing, opt)
}
}
if len(missing) > 0 {
return c, fmt.Errorf(errorMap[ErrConfigMissingOptions], missing)
}
if c.MaxBody == 0 {
c.MaxBody = 131072
}
return c, nil
}
// InitEnv initializes using the Environment (ENV)
//
// By default, it uses AKAMAI_HOST, AKAMAI_CLIENT_TOKEN, AKAMAI_CLIENT_SECRET,
// AKAMAI_ACCESS_TOKEN, and AKAMAI_MAX_BODY variables.
//
// You can define multiple configurations by prefixing with the section name specified, e.g.
// passing "ccu" will cause it to look for AKAMAI_CCU_HOST, etc.
//
// If AKAMAI_{SECTION} does not exist, it will fall back to just AKAMAI_.
func InitEnv(section string) (Config, error) {
var (
c Config
requiredOptions = []string{"HOST", "CLIENT_TOKEN", "CLIENT_SECRET", "ACCESS_TOKEN"}
missing []string
prefix string
)
// Check if section is empty
if section == "" {
section = defaultSection
} else {
section = strings.ToUpper(section)
}
prefix = "AKAMAI_"
_, ok := os.LookupEnv("AKAMAI_" + section + "_HOST")
if ok {
prefix = "AKAMAI_" + section + "_"
}
for _, opt := range requiredOptions {
val, ok := os.LookupEnv(prefix + opt)
if !ok {
missing = append(missing, prefix+opt)
} else {
switch {
case opt == "HOST":
c.Host = val
case opt == "CLIENT_TOKEN":
c.ClientToken = val
case opt == "CLIENT_SECRET":
c.ClientSecret = val
case opt == "ACCESS_TOKEN":
c.AccessToken = val
}
}
}
if len(missing) > 0 {
return c, fmt.Errorf(errorMap[ErrMissingEnvVariables], missing)
}
c.MaxBody = 0
val, ok := os.LookupEnv(prefix + "MAX_BODY")
if i, err := strconv.Atoi(val); err == nil {
c.MaxBody = i
}
if !ok || c.MaxBody == 0 {
c.MaxBody = 131072
}
return c, nil
}

View file

@ -0,0 +1,22 @@
package edgegrid
// Error constants
const (
ErrUUIDGenerateFailed = 500
ErrHomeDirNotFound = 501
ErrConfigFile = 502
ErrConfigFileSection = 503
ErrConfigMissingOptions = 504
ErrMissingEnvVariables = 505
)
var (
errorMap = map[int]string{
ErrUUIDGenerateFailed: "Generate UUID failed: %s",
ErrHomeDirNotFound: "Fatal could not find home dir from user: %s",
ErrConfigFile: "Fatal error edgegrid file: %s",
ErrConfigFileSection: "Could not map section: %s",
ErrConfigMissingOptions: "Fatal missing required options: %s",
ErrMissingEnvVariables: "Fatal missing required environment variables: %s",
}
)

View file

@ -0,0 +1,195 @@
// Package edgegrid allows you to sign http.Request's using the Akamai OPEN Edgegrid Signing Scheme
package edgegrid
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"io/ioutil"
"net/http"
"sort"
"strings"
"time"
"unicode"
log "github.com/sirupsen/logrus"
"github.com/tuvistavie/securerandom"
)
const defaultSection = "DEFAULT"
// AddRequestHeader sets the Authorization header to use Akamai Open API
func AddRequestHeader(config Config, req *http.Request) *http.Request {
if config.Debug {
log.SetLevel(log.DebugLevel)
}
timestamp := makeEdgeTimeStamp()
nonce := createNonce()
if req.Header.Get("Content-Type") == "" {
req.Header.Set("Content-Type", "application/json")
}
req.Header.Set("Authorization", createAuthHeader(config, req, timestamp, nonce))
return req
}
// Must be assigned the UTC time when the request is signed.
// Format of “yyyyMMddTHH:mm:ss+0000”
func makeEdgeTimeStamp() string {
local := time.FixedZone("GMT", 0)
t := time.Now().In(local)
return fmt.Sprintf("%d%02d%02dT%02d:%02d:%02d+0000",
t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second())
}
// Must be assigned a nonce (number used once) for the request.
// It is a random string used to detect replayed request messages.
// A GUID is recommended.
func createNonce() string {
uuid, err := securerandom.Uuid()
if err != nil {
log.Errorf(errorMap[ErrUUIDGenerateFailed], err)
return ""
}
return uuid
}
func stringMinifier(in string) (out string) {
white := false
for _, c := range in {
if unicode.IsSpace(c) {
if !white {
out = out + " "
}
white = true
} else {
out = out + string(c)
white = false
}
}
return
}
func concatPathQuery(path, query string) string {
if query == "" {
return path
}
return fmt.Sprintf("%s?%s", path, query)
}
// createSignature is the base64-encoding of the SHA256 HMAC of the data to sign with the signing key.
func createSignature(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 createHash(data string) string {
h := sha256.Sum256([]byte(data))
return base64.StdEncoding.EncodeToString(h[:])
}
func canonicalizeHeaders(config Config, req *http.Request) string {
var unsortedHeader []string
var sortedHeader []string
for k := range req.Header {
unsortedHeader = append(unsortedHeader, k)
}
sort.Strings(unsortedHeader)
for _, k := range unsortedHeader {
for _, sign := range config.HeaderToSign {
if sign == k {
v := strings.TrimSpace(req.Header.Get(k))
sortedHeader = append(sortedHeader, fmt.Sprintf("%s:%s", strings.ToLower(k), strings.ToLower(stringMinifier(v))))
}
}
}
return strings.Join(sortedHeader, "\t")
}
// signingKey is derived from the client secret.
// The signing key is computed as the base64 encoding of the SHA256 HMAC of the timestamp string
// (the field value included in the HTTP authorization header described above) with the client secret as the key.
func signingKey(config Config, timestamp string) string {
key := createSignature(timestamp, config.ClientSecret)
return key
}
// The content hash is the base64-encoded SHA256 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 createContentHash(config Config, req *http.Request) string {
var (
contentHash string
preparedBody string
bodyBytes []byte
)
if req.Body != nil {
bodyBytes, _ = ioutil.ReadAll(req.Body)
req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
preparedBody = string(bodyBytes)
}
log.Debugf("Body is %s", preparedBody)
if req.Method == "POST" && len(preparedBody) > 0 {
log.Debugf("Signing content: %s", preparedBody)
if len(preparedBody) > config.MaxBody {
log.Debugf("Data length %d is larger than maximum %d",
len(preparedBody), config.MaxBody)
preparedBody = preparedBody[0:config.MaxBody]
log.Debugf("Data truncated to %d for computing the hash", len(preparedBody))
}
contentHash = createHash(preparedBody)
}
log.Debugf("Content hash is '%s'", contentHash)
return contentHash
}
// The data to sign 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 signingData(config Config, req *http.Request, authHeader string) string {
dataSign := []string{
req.Method,
req.URL.Scheme,
req.URL.Host,
concatPathQuery(req.URL.Path, req.URL.RawQuery),
canonicalizeHeaders(config, req),
createContentHash(config, req),
authHeader,
}
log.Debugf("Data to sign %s", strings.Join(dataSign, "\t"))
return strings.Join(dataSign, "\t")
}
func signingRequest(config Config, req *http.Request, authHeader string, timestamp string) string {
return createSignature(signingData(config, req, authHeader),
signingKey(config, timestamp))
}
// 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, SHA256 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 createAuthHeader(config Config, req *http.Request, timestamp string, nonce string) string {
authHeader := fmt.Sprintf("EG1-HMAC-SHA256 client_token=%s;access_token=%s;timestamp=%s;nonce=%s;",
config.ClientToken,
config.AccessToken,
timestamp,
nonce,
)
log.Debugf("Unsigned authorization header: '%s'", authHeader)
signedAuthHeader := fmt.Sprintf("%ssignature=%s", authHeader, signingRequest(config, req, authHeader, timestamp))
log.Debugf("Signed authorization header: '%s'", signedAuthHeader)
return signedAuthHeader
}

View file

@ -0,0 +1 @@
package jsonhooks

View file

@ -0,0 +1,69 @@
// Package jsonhooks adds hooks that are automatically called before JSON marshaling (PreMarshalJSON) and
// after JSON unmarshaling (PostUnmarshalJSON). It does not do so recursively.
package jsonhooks
import (
"encoding/json"
"reflect"
)
// Marshal wraps encoding/json.Marshal, calls v.PreMarshalJSON() if it exists
func Marshal(v interface{}) ([]byte, error) {
if ImplementsPreJSONMarshaler(v) {
err := v.(PreJSONMarshaler).PreMarshalJSON()
if err != nil {
return nil, err
}
}
return json.Marshal(v)
}
// Unmarshal wraps encoding/json.Unmarshal, calls v.PostUnmarshalJSON() if it exists
func Unmarshal(data []byte, v interface{}) error {
err := json.Unmarshal(data, v)
if err != nil {
return err
}
if ImplementsPostJSONUnmarshaler(v) {
err := v.(PostJSONUnmarshaler).PostUnmarshalJSON()
if err != nil {
return err
}
}
return nil
}
// PreJSONMarshaler infers support for the PreMarshalJSON pre-hook
type PreJSONMarshaler interface {
PreMarshalJSON() error
}
// ImplementsPreJSONMarshaler checks for support for the PreMarshalJSON pre-hook
func ImplementsPreJSONMarshaler(v interface{}) bool {
value := reflect.ValueOf(v)
if value.Kind() == reflect.Ptr && value.IsNil() {
return false
}
_, ok := value.Interface().(PreJSONMarshaler)
return ok
}
// PostJSONUnmarshaler infers support for the PostUnmarshalJSON post-hook
type PostJSONUnmarshaler interface {
PostUnmarshalJSON() error
}
// ImplementsPostJSONUnmarshaler checks for support for the PostUnmarshalJSON post-hook
func ImplementsPostJSONUnmarshaler(v interface{}) bool {
value := reflect.ValueOf(v)
if value.Kind() == reflect.Ptr && value.IsNil() {
return false
}
_, ok := value.Interface().(PostJSONUnmarshaler)
return ok
}