Merge tag 'v1.7.4' into master

This commit is contained in:
Fernandez Ludovic 2018-10-30 12:34:00 +01:00
commit d3ae88f108
154 changed files with 4356 additions and 1285 deletions

View file

@ -0,0 +1,408 @@
package auth
// Copyright 2017 Microsoft Corporation
//
// 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.
import (
"bytes"
"crypto/rsa"
"crypto/x509"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"strings"
"unicode/utf16"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/dimchansky/utfbom"
"golang.org/x/crypto/pkcs12"
)
// NewAuthorizerFromEnvironment creates an Authorizer configured from environment variables in the order:
// 1. Client credentials
// 2. Client certificate
// 3. Username password
// 4. MSI
func NewAuthorizerFromEnvironment() (autorest.Authorizer, error) {
tenantID := os.Getenv("AZURE_TENANT_ID")
clientID := os.Getenv("AZURE_CLIENT_ID")
clientSecret := os.Getenv("AZURE_CLIENT_SECRET")
certificatePath := os.Getenv("AZURE_CERTIFICATE_PATH")
certificatePassword := os.Getenv("AZURE_CERTIFICATE_PASSWORD")
username := os.Getenv("AZURE_USERNAME")
password := os.Getenv("AZURE_PASSWORD")
envName := os.Getenv("AZURE_ENVIRONMENT")
resource := os.Getenv("AZURE_AD_RESOURCE")
var env azure.Environment
if envName == "" {
env = azure.PublicCloud
} else {
var err error
env, err = azure.EnvironmentFromName(envName)
if err != nil {
return nil, err
}
}
if resource == "" {
resource = env.ResourceManagerEndpoint
}
//1.Client Credentials
if clientSecret != "" {
config := NewClientCredentialsConfig(clientID, clientSecret, tenantID)
config.AADEndpoint = env.ActiveDirectoryEndpoint
config.Resource = resource
return config.Authorizer()
}
//2. Client Certificate
if certificatePath != "" {
config := NewClientCertificateConfig(certificatePath, certificatePassword, clientID, tenantID)
config.AADEndpoint = env.ActiveDirectoryEndpoint
config.Resource = resource
return config.Authorizer()
}
//3. Username Password
if username != "" && password != "" {
config := NewUsernamePasswordConfig(username, password, clientID, tenantID)
config.AADEndpoint = env.ActiveDirectoryEndpoint
config.Resource = resource
return config.Authorizer()
}
// 4. MSI
config := NewMSIConfig()
config.Resource = resource
config.ClientID = clientID
return config.Authorizer()
}
// NewAuthorizerFromFile creates an Authorizer configured from a configuration file.
func NewAuthorizerFromFile(baseURI string) (autorest.Authorizer, error) {
fileLocation := os.Getenv("AZURE_AUTH_LOCATION")
if fileLocation == "" {
return nil, errors.New("auth file not found. Environment variable AZURE_AUTH_LOCATION is not set")
}
contents, err := ioutil.ReadFile(fileLocation)
if err != nil {
return nil, err
}
// Auth file might be encoded
decoded, err := decode(contents)
if err != nil {
return nil, err
}
file := file{}
err = json.Unmarshal(decoded, &file)
if err != nil {
return nil, err
}
resource, err := getResourceForToken(file, baseURI)
if err != nil {
return nil, err
}
config, err := adal.NewOAuthConfig(file.ActiveDirectoryEndpoint, file.TenantID)
if err != nil {
return nil, err
}
spToken, err := adal.NewServicePrincipalToken(*config, file.ClientID, file.ClientSecret, resource)
if err != nil {
return nil, err
}
return autorest.NewBearerAuthorizer(spToken), nil
}
// File represents the authentication file
type file struct {
ClientID string `json:"clientId,omitempty"`
ClientSecret string `json:"clientSecret,omitempty"`
SubscriptionID string `json:"subscriptionId,omitempty"`
TenantID string `json:"tenantId,omitempty"`
ActiveDirectoryEndpoint string `json:"activeDirectoryEndpointUrl,omitempty"`
ResourceManagerEndpoint string `json:"resourceManagerEndpointUrl,omitempty"`
GraphResourceID string `json:"activeDirectoryGraphResourceId,omitempty"`
SQLManagementEndpoint string `json:"sqlManagementEndpointUrl,omitempty"`
GalleryEndpoint string `json:"galleryEndpointUrl,omitempty"`
ManagementEndpoint string `json:"managementEndpointUrl,omitempty"`
}
func decode(b []byte) ([]byte, error) {
reader, enc := utfbom.Skip(bytes.NewReader(b))
switch enc {
case utfbom.UTF16LittleEndian:
u16 := make([]uint16, (len(b)/2)-1)
err := binary.Read(reader, binary.LittleEndian, &u16)
if err != nil {
return nil, err
}
return []byte(string(utf16.Decode(u16))), nil
case utfbom.UTF16BigEndian:
u16 := make([]uint16, (len(b)/2)-1)
err := binary.Read(reader, binary.BigEndian, &u16)
if err != nil {
return nil, err
}
return []byte(string(utf16.Decode(u16))), nil
}
return ioutil.ReadAll(reader)
}
func getResourceForToken(f file, baseURI string) (string, error) {
// Compare dafault base URI from the SDK to the endpoints from the public cloud
// Base URI and token resource are the same string. This func finds the authentication
// file field that matches the SDK base URI. The SDK defines the public cloud
// endpoint as its default base URI
if !strings.HasSuffix(baseURI, "/") {
baseURI += "/"
}
switch baseURI {
case azure.PublicCloud.ServiceManagementEndpoint:
return f.ManagementEndpoint, nil
case azure.PublicCloud.ResourceManagerEndpoint:
return f.ResourceManagerEndpoint, nil
case azure.PublicCloud.ActiveDirectoryEndpoint:
return f.ActiveDirectoryEndpoint, nil
case azure.PublicCloud.GalleryEndpoint:
return f.GalleryEndpoint, nil
case azure.PublicCloud.GraphEndpoint:
return f.GraphResourceID, nil
}
return "", fmt.Errorf("auth: base URI not found in endpoints")
}
// NewClientCredentialsConfig creates an AuthorizerConfig object configured to obtain an Authorizer through Client Credentials.
// Defaults to Public Cloud and Resource Manager Endpoint.
func NewClientCredentialsConfig(clientID string, clientSecret string, tenantID string) ClientCredentialsConfig {
return ClientCredentialsConfig{
ClientID: clientID,
ClientSecret: clientSecret,
TenantID: tenantID,
Resource: azure.PublicCloud.ResourceManagerEndpoint,
AADEndpoint: azure.PublicCloud.ActiveDirectoryEndpoint,
}
}
// NewClientCertificateConfig creates a ClientCertificateConfig object configured to obtain an Authorizer through client certificate.
// Defaults to Public Cloud and Resource Manager Endpoint.
func NewClientCertificateConfig(certificatePath string, certificatePassword string, clientID string, tenantID string) ClientCertificateConfig {
return ClientCertificateConfig{
CertificatePath: certificatePath,
CertificatePassword: certificatePassword,
ClientID: clientID,
TenantID: tenantID,
Resource: azure.PublicCloud.ResourceManagerEndpoint,
AADEndpoint: azure.PublicCloud.ActiveDirectoryEndpoint,
}
}
// NewUsernamePasswordConfig creates an UsernamePasswordConfig object configured to obtain an Authorizer through username and password.
// Defaults to Public Cloud and Resource Manager Endpoint.
func NewUsernamePasswordConfig(username string, password string, clientID string, tenantID string) UsernamePasswordConfig {
return UsernamePasswordConfig{
Username: username,
Password: password,
ClientID: clientID,
TenantID: tenantID,
Resource: azure.PublicCloud.ResourceManagerEndpoint,
AADEndpoint: azure.PublicCloud.ActiveDirectoryEndpoint,
}
}
// NewMSIConfig creates an MSIConfig object configured to obtain an Authorizer through MSI.
func NewMSIConfig() MSIConfig {
return MSIConfig{
Resource: azure.PublicCloud.ResourceManagerEndpoint,
}
}
// NewDeviceFlowConfig creates a DeviceFlowConfig object configured to obtain an Authorizer through device flow.
// Defaults to Public Cloud and Resource Manager Endpoint.
func NewDeviceFlowConfig(clientID string, tenantID string) DeviceFlowConfig {
return DeviceFlowConfig{
ClientID: clientID,
TenantID: tenantID,
Resource: azure.PublicCloud.ResourceManagerEndpoint,
AADEndpoint: azure.PublicCloud.ActiveDirectoryEndpoint,
}
}
//AuthorizerConfig provides an authorizer from the configuration provided.
type AuthorizerConfig interface {
Authorizer() (autorest.Authorizer, error)
}
// ClientCredentialsConfig provides the options to get a bearer authorizer from client credentials.
type ClientCredentialsConfig struct {
ClientID string
ClientSecret string
TenantID string
AADEndpoint string
Resource string
}
// Authorizer gets the authorizer from client credentials.
func (ccc ClientCredentialsConfig) Authorizer() (autorest.Authorizer, error) {
oauthConfig, err := adal.NewOAuthConfig(ccc.AADEndpoint, ccc.TenantID)
if err != nil {
return nil, err
}
spToken, err := adal.NewServicePrincipalToken(*oauthConfig, ccc.ClientID, ccc.ClientSecret, ccc.Resource)
if err != nil {
return nil, fmt.Errorf("failed to get oauth token from client credentials: %v", err)
}
return autorest.NewBearerAuthorizer(spToken), nil
}
// ClientCertificateConfig provides the options to get a bearer authorizer from a client certificate.
type ClientCertificateConfig struct {
ClientID string
CertificatePath string
CertificatePassword string
TenantID string
AADEndpoint string
Resource string
}
// Authorizer gets an authorizer object from client certificate.
func (ccc ClientCertificateConfig) Authorizer() (autorest.Authorizer, error) {
oauthConfig, err := adal.NewOAuthConfig(ccc.AADEndpoint, ccc.TenantID)
certData, err := ioutil.ReadFile(ccc.CertificatePath)
if err != nil {
return nil, fmt.Errorf("failed to read the certificate file (%s): %v", ccc.CertificatePath, err)
}
certificate, rsaPrivateKey, err := decodePkcs12(certData, ccc.CertificatePassword)
if err != nil {
return nil, fmt.Errorf("failed to decode pkcs12 certificate while creating spt: %v", err)
}
spToken, err := adal.NewServicePrincipalTokenFromCertificate(*oauthConfig, ccc.ClientID, certificate, rsaPrivateKey, ccc.Resource)
if err != nil {
return nil, fmt.Errorf("failed to get oauth token from certificate auth: %v", err)
}
return autorest.NewBearerAuthorizer(spToken), nil
}
// DeviceFlowConfig provides the options to get a bearer authorizer using device flow authentication.
type DeviceFlowConfig struct {
ClientID string
TenantID string
AADEndpoint string
Resource string
}
// Authorizer gets the authorizer from device flow.
func (dfc DeviceFlowConfig) Authorizer() (autorest.Authorizer, error) {
oauthClient := &autorest.Client{}
oauthConfig, err := adal.NewOAuthConfig(dfc.AADEndpoint, dfc.TenantID)
deviceCode, err := adal.InitiateDeviceAuth(oauthClient, *oauthConfig, dfc.ClientID, dfc.AADEndpoint)
if err != nil {
return nil, fmt.Errorf("failed to start device auth flow: %s", err)
}
log.Println(*deviceCode.Message)
token, err := adal.WaitForUserCompletion(oauthClient, deviceCode)
if err != nil {
return nil, fmt.Errorf("failed to finish device auth flow: %s", err)
}
spToken, err := adal.NewServicePrincipalTokenFromManualToken(*oauthConfig, dfc.ClientID, dfc.Resource, *token)
if err != nil {
return nil, fmt.Errorf("failed to get oauth token from device flow: %v", err)
}
return autorest.NewBearerAuthorizer(spToken), nil
}
func decodePkcs12(pkcs []byte, password string) (*x509.Certificate, *rsa.PrivateKey, error) {
privateKey, certificate, err := pkcs12.Decode(pkcs, password)
if err != nil {
return nil, nil, err
}
rsaPrivateKey, isRsaKey := privateKey.(*rsa.PrivateKey)
if !isRsaKey {
return nil, nil, fmt.Errorf("PKCS#12 certificate must contain an RSA private key")
}
return certificate, rsaPrivateKey, nil
}
// UsernamePasswordConfig provides the options to get a bearer authorizer from a username and a password.
type UsernamePasswordConfig struct {
ClientID string
Username string
Password string
TenantID string
AADEndpoint string
Resource string
}
// Authorizer gets the authorizer from a username and a password.
func (ups UsernamePasswordConfig) Authorizer() (autorest.Authorizer, error) {
oauthConfig, err := adal.NewOAuthConfig(ups.AADEndpoint, ups.TenantID)
spToken, err := adal.NewServicePrincipalTokenFromUsernamePassword(*oauthConfig, ups.ClientID, ups.Username, ups.Password, ups.Resource)
if err != nil {
return nil, fmt.Errorf("failed to get oauth token from username and password auth: %v", err)
}
return autorest.NewBearerAuthorizer(spToken), nil
}
// MSIConfig provides the options to get a bearer authorizer through MSI.
type MSIConfig struct {
Resource string
ClientID string
}
// Authorizer gets the authorizer from MSI.
func (mc MSIConfig) Authorizer() (autorest.Authorizer, error) {
msiEndpoint, err := adal.GetMSIVMEndpoint()
if err != nil {
return nil, err
}
spToken, err := adal.NewServicePrincipalTokenFromMSI(msiEndpoint, mc.Resource)
if err != nil {
return nil, fmt.Errorf("failed to get oauth token from MSI: %v", err)
}
return autorest.NewBearerAuthorizer(spToken), nil
}

View file

@ -53,6 +53,12 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash,
varsN := make([]string, len(idxs)/2)
varsR := make([]*regexp.Regexp, len(idxs)/2)
pattern := bytes.NewBufferString("")
// Host matching is case insensitive
if matchHost {
fmt.Fprint(pattern, "(?i)")
}
pattern.WriteByte('^')
reverse := bytes.NewBufferString("")
var end int

201
vendor/github.com/dimchansky/utfbom/LICENSE generated vendored Normal file
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.

174
vendor/github.com/dimchansky/utfbom/utfbom.go generated vendored Normal file
View file

@ -0,0 +1,174 @@
// Package utfbom implements the detection of the BOM (Unicode Byte Order Mark) and removing as necessary.
// It wraps an io.Reader object, creating another object (Reader) that also implements the io.Reader
// interface but provides automatic BOM checking and removing as necessary.
package utfbom
import (
"errors"
"io"
)
// Encoding is type alias for detected UTF encoding.
type Encoding int
// Constants to identify detected UTF encodings.
const (
// Unknown encoding, returned when no BOM was detected
Unknown Encoding = iota
// UTF8, BOM bytes: EF BB BF
UTF8
// UTF-16, big-endian, BOM bytes: FE FF
UTF16BigEndian
// UTF-16, little-endian, BOM bytes: FF FE
UTF16LittleEndian
// UTF-32, big-endian, BOM bytes: 00 00 FE FF
UTF32BigEndian
// UTF-32, little-endian, BOM bytes: FF FE 00 00
UTF32LittleEndian
)
const maxConsecutiveEmptyReads = 100
// Skip creates Reader which automatically detects BOM (Unicode Byte Order Mark) and removes it as necessary.
// It also returns the encoding detected by the BOM.
// If the detected encoding is not needed, you can call the SkipOnly function.
func Skip(rd io.Reader) (*Reader, Encoding) {
// Is it already a Reader?
b, ok := rd.(*Reader)
if ok {
return b, Unknown
}
enc, left, err := detectUtf(rd)
return &Reader{
rd: rd,
buf: left,
err: err,
}, enc
}
// SkipOnly creates Reader which automatically detects BOM (Unicode Byte Order Mark) and removes it as necessary.
func SkipOnly(rd io.Reader) *Reader {
r, _ := Skip(rd)
return r
}
// Reader implements automatic BOM (Unicode Byte Order Mark) checking and
// removing as necessary for an io.Reader object.
type Reader struct {
rd io.Reader // reader provided by the client
buf []byte // buffered data
err error // last error
}
// Read is an implementation of io.Reader interface.
// The bytes are taken from the underlying Reader, but it checks for BOMs, removing them as necessary.
func (r *Reader) Read(p []byte) (n int, err error) {
if len(p) == 0 {
return 0, nil
}
if r.buf == nil {
if r.err != nil {
return 0, r.readErr()
}
return r.rd.Read(p)
}
// copy as much as we can
n = copy(p, r.buf)
r.buf = nilIfEmpty(r.buf[n:])
return n, nil
}
func (r *Reader) readErr() error {
err := r.err
r.err = nil
return err
}
var errNegativeRead = errors.New("utfbom: reader returned negative count from Read")
func detectUtf(rd io.Reader) (enc Encoding, buf []byte, err error) {
buf, err = readBOM(rd)
if len(buf) >= 4 {
if isUTF32BigEndianBOM4(buf) {
return UTF32BigEndian, nilIfEmpty(buf[4:]), err
}
if isUTF32LittleEndianBOM4(buf) {
return UTF32LittleEndian, nilIfEmpty(buf[4:]), err
}
}
if len(buf) > 2 && isUTF8BOM3(buf) {
return UTF8, nilIfEmpty(buf[3:]), err
}
if (err != nil && err != io.EOF) || (len(buf) < 2) {
return Unknown, nilIfEmpty(buf), err
}
if isUTF16BigEndianBOM2(buf) {
return UTF16BigEndian, nilIfEmpty(buf[2:]), err
}
if isUTF16LittleEndianBOM2(buf) {
return UTF16LittleEndian, nilIfEmpty(buf[2:]), err
}
return Unknown, nilIfEmpty(buf), err
}
func readBOM(rd io.Reader) (buf []byte, err error) {
const maxBOMSize = 4
var bom [maxBOMSize]byte // used to read BOM
// read as many bytes as possible
for nEmpty, n := 0, 0; err == nil && len(buf) < maxBOMSize; buf = bom[:len(buf)+n] {
if n, err = rd.Read(bom[len(buf):]); n < 0 {
panic(errNegativeRead)
}
if n > 0 {
nEmpty = 0
} else {
nEmpty++
if nEmpty >= maxConsecutiveEmptyReads {
err = io.ErrNoProgress
}
}
}
return
}
func isUTF32BigEndianBOM4(buf []byte) bool {
return buf[0] == 0x00 && buf[1] == 0x00 && buf[2] == 0xFE && buf[3] == 0xFF
}
func isUTF32LittleEndianBOM4(buf []byte) bool {
return buf[0] == 0xFF && buf[1] == 0xFE && buf[2] == 0x00 && buf[3] == 0x00
}
func isUTF8BOM3(buf []byte) bool {
return buf[0] == 0xEF && buf[1] == 0xBB && buf[2] == 0xBF
}
func isUTF16BigEndianBOM2(buf []byte) bool {
return buf[0] == 0xFE && buf[1] == 0xFF
}
func isUTF16LittleEndianBOM2(buf []byte) bool {
return buf[0] == 0xFF && buf[1] == 0xFE
}
func nilIfEmpty(buf []byte) (res []byte) {
if len(buf) > 0 {
res = buf
}
return
}

View file

@ -1,22 +0,0 @@
package auroradnsclient
import (
"github.com/edeckers/auroradnsclient/requests"
)
// AuroraDNSClient is a client for accessing the Aurora DNS API
type AuroraDNSClient struct {
requestor *requests.AuroraRequestor
}
// NewAuroraDNSClient instantiates a new client
func NewAuroraDNSClient(endpoint string, userID string, key string) (*AuroraDNSClient, error) {
requestor, err := requests.NewAuroraRequestor(endpoint, userID, key)
if err != nil {
return nil, err
}
return &AuroraDNSClient{
requestor: requestor,
}, nil
}

View file

@ -1,11 +0,0 @@
package auroradnsclient
// AuroraDNSError describes the format of a generic AuroraDNS API error
type AuroraDNSError struct {
ErrorCode string `json:"error"`
Message string `json:"errormsg"`
}
func (e AuroraDNSError) Error() string {
return e.Message
}

View file

@ -1,75 +0,0 @@
package auroradnsclient
import (
"encoding/json"
"fmt"
"github.com/edeckers/auroradnsclient/records"
"github.com/sirupsen/logrus"
)
// GetRecords returns a list of all records in given zone
func (client *AuroraDNSClient) GetRecords(zoneID string) ([]records.GetRecordsResponse, error) {
logrus.Debugf("GetRecords(%s)", zoneID)
relativeURL := fmt.Sprintf("zones/%s/records", zoneID)
response, err := client.requestor.Request(relativeURL, "GET", []byte(""))
if err != nil {
logrus.Errorf("Failed to receive records: %s", err)
return nil, err
}
var respData []records.GetRecordsResponse
err = json.Unmarshal(response, &respData)
if err != nil {
logrus.Errorf("Failed to unmarshall response: %s", err)
return nil, err
}
return respData, nil
}
// CreateRecord creates a new record in given zone
func (client *AuroraDNSClient) CreateRecord(zoneID string, data records.CreateRecordRequest) (*records.CreateRecordResponse, error) {
logrus.Debugf("CreateRecord(%s, %+v)", zoneID, data)
body, err := json.Marshal(data)
if err != nil {
logrus.Errorf("Failed to marshall request body: %s", err)
return nil, err
}
relativeURL := fmt.Sprintf("zones/%s/records", zoneID)
response, err := client.requestor.Request(relativeURL, "POST", body)
if err != nil {
logrus.Errorf("Failed to create record: %s", err)
return nil, err
}
var respData *records.CreateRecordResponse
err = json.Unmarshal(response, &respData)
if err != nil {
logrus.Errorf("Failed to unmarshall response: %s", err)
return nil, err
}
return respData, nil
}
// RemoveRecord removes a record corresponding to a particular id in a given zone
func (client *AuroraDNSClient) RemoveRecord(zoneID string, recordID string) (*records.RemoveRecordResponse, error) {
logrus.Debugf("RemoveRecord(%s, %s)", zoneID, recordID)
relativeURL := fmt.Sprintf("zones/%s/records/%s", zoneID, recordID)
_, err := client.requestor.Request(relativeURL, "DELETE", nil)
if err != nil {
logrus.Errorf("Failed to remove record: %s", err)
return nil, err
}
return &records.RemoveRecordResponse{}, nil
}

View file

@ -1,31 +0,0 @@
package records
// CreateRecordRequest describes the json payload for creating a record
type CreateRecordRequest struct {
RecordType string `json:"type"`
Name string `json:"name"`
Content string `json:"content"`
TTL int `json:"ttl"`
}
// CreateRecordResponse describes the json response for creating a record
type CreateRecordResponse struct {
ID string `json:"id"`
RecordType string `json:"type"`
Name string `json:"name"`
Content string `json:"content"`
TTL int `json:"ttl"`
}
// GetRecordsResponse describes the json response of a single record
type GetRecordsResponse struct {
ID string `json:"id"`
RecordType string `json:"type"`
Name string `json:"name"`
Content string `json:"content"`
TTL int `json:"ttl"`
}
// RemoveRecordResponse describes the json response for removing a record
type RemoveRecordResponse struct {
}

View file

@ -1,19 +0,0 @@
package errors
// BadRequest HTTP error wrapper
type BadRequest error
// Unauthorized HTTP error wrapper
type Unauthorized error
// Forbidden HTTP error wrapper
type Forbidden error
// NotFound HTTP error wrapper
type NotFound error
// ServerError HTTP error wrapper
type ServerError error
// InvalidStatusCodeError is used when none of the other types applies
type InvalidStatusCodeError error

View file

@ -1,124 +0,0 @@
package requests
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/http/httputil"
"time"
request_errors "github.com/edeckers/auroradnsclient/requests/errors"
"github.com/edeckers/auroradnsclient/tokens"
"github.com/sirupsen/logrus"
)
// AuroraRequestor performs actual requests to API
type AuroraRequestor struct {
endpoint string
userID string
key string
}
// NewAuroraRequestor instantiates a new requestor
func NewAuroraRequestor(endpoint string, userID string, key string) (*AuroraRequestor, error) {
if endpoint == "" {
return nil, fmt.Errorf("Aurora endpoint missing")
}
if userID == "" || key == "" {
return nil, fmt.Errorf("Aurora credentials missing")
}
return &AuroraRequestor{endpoint: endpoint, userID: userID, key: key}, nil
}
func (requestor *AuroraRequestor) buildRequest(relativeURL string, method string, body []byte) (*http.Request, error) {
url := fmt.Sprintf("%s/%s", requestor.endpoint, relativeURL)
request, err := http.NewRequest(method, url, bytes.NewReader(body))
if err != nil {
logrus.Errorf("Failed to build request: %s", err)
return request, err
}
timestamp := time.Now().UTC()
fmtTime := timestamp.Format("20060102T150405Z")
token := tokens.NewToken(requestor.userID, requestor.key, method, fmt.Sprintf("/%s", relativeURL), timestamp)
request.Header.Set("X-AuroraDNS-Date", fmtTime)
request.Header.Set("Authorization", fmt.Sprintf("AuroraDNSv1 %s", token))
request.Header.Set("Content-Type", "application/json")
rawRequest, err := httputil.DumpRequestOut(request, true)
if err != nil {
logrus.Errorf("Failed to dump request: %s", err)
}
logrus.Debugf("Built request:\n%s", rawRequest)
return request, err
}
func (requestor *AuroraRequestor) testInvalidResponse(resp *http.Response, response []byte) ([]byte, error) {
if resp.StatusCode < 400 {
return response, nil
}
logrus.Errorf("Received invalid status code %d:\n%s", resp.StatusCode, response)
content := errors.New(string(response))
statusCodeErrorMap := map[int]error{
400: request_errors.BadRequest(content),
401: request_errors.Unauthorized(content),
403: request_errors.Forbidden(content),
404: request_errors.NotFound(content),
500: request_errors.ServerError(content),
}
mappedError := statusCodeErrorMap[resp.StatusCode]
if mappedError == nil {
return nil, request_errors.InvalidStatusCodeError(content)
}
return nil, mappedError
}
// Request builds and executues a request to the API
func (requestor *AuroraRequestor) Request(relativeURL string, method string, body []byte) ([]byte, error) {
req, err := requestor.buildRequest(relativeURL, method, body)
client := http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
logrus.Errorf("Failed request: %s", err)
return nil, err
}
defer resp.Body.Close()
rawResponse, err := httputil.DumpResponse(resp, true)
logrus.Debugf("Received raw response:\n%s", rawResponse)
if err != nil {
logrus.Errorf("Failed to dump response: %s", err)
}
response, err := ioutil.ReadAll(resp.Body)
if err != nil {
logrus.Errorf("Failed to read response: %s", response)
return nil, err
}
response, err = requestor.testInvalidResponse(resp, response)
if err != nil {
return nil, err
}
return response, nil
}

View file

@ -1,35 +0,0 @@
package tokens
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"strings"
"time"
"github.com/sirupsen/logrus"
)
// NewToken generates a token for accessing a specific method of the API
func NewToken(userID string, key string, method string, action string, timestamp time.Time) string {
fmtTime := timestamp.Format("20060102T150405Z")
logrus.Debugf("Built timestamp: %s", fmtTime)
message := strings.Join([]string{method, action, fmtTime}, "")
logrus.Debugf("Built message: %s", message)
signatureHmac := hmac.New(sha256.New, []byte(key))
signatureHmac.Write([]byte(message))
signature := base64.StdEncoding.EncodeToString([]byte(signatureHmac.Sum(nil)))
logrus.Debugf("Built signature: %s", signature)
userIDAndSignature := fmt.Sprintf("%s:%s", userID, signature)
token := base64.StdEncoding.EncodeToString([]byte(userIDAndSignature))
logrus.Debugf("Built token: %s", token)
return token
}

View file

@ -1,29 +0,0 @@
package auroradnsclient
import (
"encoding/json"
"github.com/edeckers/auroradnsclient/zones"
"github.com/sirupsen/logrus"
)
// GetZones returns a list of all zones
func (client *AuroraDNSClient) GetZones() ([]zones.ZoneRecord, error) {
logrus.Debugf("GetZones")
response, err := client.requestor.Request("zones", "GET", []byte(""))
if err != nil {
logrus.Errorf("Failed to get zones: %s", err)
return nil, err
}
var respData []zones.ZoneRecord
err = json.Unmarshal(response, &respData)
if err != nil {
logrus.Errorf("Failed to unmarshall response: %s", err)
return nil, err
}
logrus.Debugf("Unmarshalled response: %+v", respData)
return respData, nil
}

View file

@ -1,7 +0,0 @@
package zones
// ZoneRecord describes the json format for a zone
type ZoneRecord struct {
ID string `json:"id"`
Name string `json:"name"`
}

98
vendor/github.com/ldez/go-auroradns/auth.go generated vendored Normal file
View file

@ -0,0 +1,98 @@
package auroradns
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"net/http"
"strings"
"time"
)
// TokenTransport HTTP transport for API authentication
type TokenTransport struct {
userID string
key string
// Transport is the underlying HTTP transport to use when making requests.
// It will default to http.DefaultTransport if nil.
Transport http.RoundTripper
}
// NewTokenTransport Creates a new TokenTransport
func NewTokenTransport(userID, key string) (*TokenTransport, error) {
if userID == "" || key == "" {
return nil, fmt.Errorf("credentials missing")
}
return &TokenTransport{userID: userID, key: key}, nil
}
// RoundTrip executes a single HTTP transaction
func (t *TokenTransport) RoundTrip(req *http.Request) (*http.Response, error) {
enrichedReq := &http.Request{}
*enrichedReq = *req
enrichedReq.Header = make(http.Header, len(req.Header))
for k, s := range req.Header {
enrichedReq.Header[k] = append([]string(nil), s...)
}
if t.userID != "" && t.key != "" {
timestamp := time.Now().UTC()
fmtTime := timestamp.Format("20060102T150405Z")
req.Header.Set("X-AuroraDNS-Date", fmtTime)
token, err := newToken(t.userID, t.key, req.Method, req.URL.Path, timestamp)
if err == nil {
req.Header.Set("Authorization", fmt.Sprintf("AuroraDNSv1 %s", token))
}
}
return t.transport().RoundTrip(enrichedReq)
}
// Wrap Wrap a HTTP client Transport with the TokenTransport
func (t *TokenTransport) Wrap(client *http.Client) *http.Client {
backup := client.Transport
t.Transport = backup
client.Transport = t
return client
}
// Client Creates a new HTTP client
func (t *TokenTransport) Client() *http.Client {
return &http.Client{
Transport: t,
Timeout: 30 * time.Second,
}
}
func (t *TokenTransport) transport() http.RoundTripper {
if t.Transport != nil {
return t.Transport
}
return http.DefaultTransport
}
// newToken generates a token for accessing a specific method of the API
func newToken(userID string, key string, method string, action string, timestamp time.Time) (string, error) {
fmtTime := timestamp.Format("20060102T150405Z")
message := strings.Join([]string{method, action, fmtTime}, "")
signatureHmac := hmac.New(sha256.New, []byte(key))
_, err := signatureHmac.Write([]byte(message))
if err != nil {
return "", err
}
signature := base64.StdEncoding.EncodeToString(signatureHmac.Sum(nil))
userIDAndSignature := fmt.Sprintf("%s:%s", userID, signature)
token := base64.StdEncoding.EncodeToString([]byte(userIDAndSignature))
return token, nil
}

144
vendor/github.com/ldez/go-auroradns/client.go generated vendored Normal file
View file

@ -0,0 +1,144 @@
package auroradns
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
)
const defaultBaseURL = "https://api.auroradns.eu"
const (
contentTypeHeader = "Content-Type"
contentTypeJSON = "application/json"
)
// ErrorResponse A representation of an API error message.
type ErrorResponse struct {
ErrorCode string `json:"error"`
Message string `json:"errormsg"`
}
func (e *ErrorResponse) Error() string {
return fmt.Sprintf("%s - %s", e.ErrorCode, e.Message)
}
// Option Type of a client option
type Option func(*Client) error
// Client The API client
type Client struct {
baseURL *url.URL
UserAgent string
httpClient *http.Client
}
// NewClient Creates a new client
func NewClient(httpClient *http.Client, opts ...Option) (*Client, error) {
if httpClient == nil {
httpClient = http.DefaultClient
}
baseURL, _ := url.Parse(defaultBaseURL)
client := &Client{
baseURL: baseURL,
httpClient: httpClient,
}
for _, opt := range opts {
err := opt(client)
if err != nil {
return nil, err
}
}
return client, nil
}
func (c *Client) newRequest(method, resource string, body io.Reader) (*http.Request, error) {
u, err := c.baseURL.Parse(resource)
if err != nil {
return nil, err
}
req, err := http.NewRequest(method, u.String(), body)
if err != nil {
return nil, err
}
req.Header.Set(contentTypeHeader, contentTypeJSON)
if c.UserAgent != "" {
req.Header.Set("User-Agent", c.UserAgent)
}
return req, nil
}
func (c *Client) do(req *http.Request, v interface{}) (*http.Response, error) {
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer func() { _ = resp.Body.Close() }()
if err = checkResponse(resp); err != nil {
return resp, err
}
if v == nil {
return resp, nil
}
raw, err := ioutil.ReadAll(resp.Body)
if err != nil {
return resp, fmt.Errorf("failed to read body: %v", err)
}
if err = json.Unmarshal(raw, v); err != nil {
return resp, fmt.Errorf("unmarshaling %T error: %v: %s", err, v, string(raw))
}
return resp, nil
}
func checkResponse(resp *http.Response) error {
if c := resp.StatusCode; 200 <= c && c <= 299 {
return nil
}
data, err := ioutil.ReadAll(resp.Body)
if err == nil && data != nil {
errorResponse := new(ErrorResponse)
err = json.Unmarshal(data, errorResponse)
if err != nil {
return fmt.Errorf("unmarshaling ErrorResponse error: %v: %s", err.Error(), string(data))
}
return errorResponse
}
defer func() { _ = resp.Body.Close() }()
return nil
}
// WithBaseURL Allows to define a custom base URL
func WithBaseURL(rawBaseURL string) func(*Client) error {
return func(client *Client) error {
if len(rawBaseURL) == 0 {
return nil
}
baseURL, err := url.Parse(rawBaseURL)
if err != nil {
return err
}
client.baseURL = baseURL
return nil
}
}

91
vendor/github.com/ldez/go-auroradns/records.go generated vendored Normal file
View file

@ -0,0 +1,91 @@
package auroradns
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
// Record types
const (
RecordTypeA = "A"
RecordTypeAAAA = "AAAA"
RecordTypeCNAME = "CNAME"
RecordTypeMX = "MX"
RecordTypeNS = "NS"
RecordTypeSOA = "SOA"
RecordTypeSRV = "SRV"
RecordTypeTXT = "TXT"
RecordTypeDS = "DS"
RecordTypePTR = "PTR"
RecordTypeSSHFP = "SSHFP"
RecordTypeTLSA = "TLS"
)
// Record a DNS record
type Record struct {
ID string `json:"id,omitempty"`
RecordType string `json:"type"`
Name string `json:"name"`
Content string `json:"content"`
TTL int `json:"ttl,omitempty"`
}
// CreateRecord Creates a new record.
func (c *Client) CreateRecord(zoneID string, record Record) (*Record, *http.Response, error) {
body, err := json.Marshal(record)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshall request body: %v", err)
}
resource := fmt.Sprintf("/zones/%s/records", zoneID)
req, err := c.newRequest(http.MethodPost, resource, bytes.NewReader(body))
if err != nil {
return nil, nil, err
}
newRecord := new(Record)
resp, err := c.do(req, newRecord)
if err != nil {
return nil, resp, err
}
return newRecord, resp, nil
}
// DeleteRecord Delete a record.
func (c *Client) DeleteRecord(zoneID string, recordID string) (bool, *http.Response, error) {
resource := fmt.Sprintf("/zones/%s/records/%s", zoneID, recordID)
req, err := c.newRequest(http.MethodDelete, resource, nil)
if err != nil {
return false, nil, err
}
resp, err := c.do(req, nil)
if err != nil {
return false, resp, err
}
return true, resp, nil
}
// ListRecords returns a list of all records in given zone
func (c *Client) ListRecords(zoneID string) ([]Record, *http.Response, error) {
resource := fmt.Sprintf("/zones/%s/records", zoneID)
req, err := c.newRequest(http.MethodGet, resource, nil)
if err != nil {
return nil, nil, err
}
var records []Record
resp, err := c.do(req, &records)
if err != nil {
return nil, resp, err
}
return records, resp, nil
}

69
vendor/github.com/ldez/go-auroradns/zones.go generated vendored Normal file
View file

@ -0,0 +1,69 @@
package auroradns
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
// Zone a DNS zone
type Zone struct {
ID string `json:"id,omitempty"`
Name string `json:"name"`
}
// CreateZone Creates a zone.
func (c *Client) CreateZone(domain string) (*Zone, *http.Response, error) {
body, err := json.Marshal(Zone{Name: domain})
if err != nil {
return nil, nil, fmt.Errorf("failed to marshall request body: %v", err)
}
req, err := c.newRequest(http.MethodPost, "/zones", bytes.NewReader(body))
if err != nil {
return nil, nil, err
}
zone := new(Zone)
resp, err := c.do(req, zone)
if err != nil {
return nil, resp, err
}
return zone, resp, nil
}
// DeleteZone Delete a zone.
func (c *Client) DeleteZone(zoneID string) (bool, *http.Response, error) {
resource := fmt.Sprintf("/zones/%s", zoneID)
req, err := c.newRequest(http.MethodDelete, resource, nil)
if err != nil {
return false, nil, err
}
resp, err := c.do(req, nil)
if err != nil {
return false, resp, err
}
return true, resp, nil
}
// ListZones returns a list of all zones.
func (c *Client) ListZones() ([]Zone, *http.Response, error) {
req, err := c.newRequest(http.MethodGet, "/zones", nil)
if err != nil {
return nil, nil, err
}
var zones []Zone
resp, err := c.do(req, &zones)
if err != nil {
return nil, resp, err
}
return zones, resp, nil
}

View file

@ -7,6 +7,7 @@ import (
"fmt"
"net"
"strings"
"sync"
"time"
"github.com/miekg/dns"
@ -18,8 +19,9 @@ type preCheckDNSFunc func(fqdn, value string) (bool, error)
var (
// PreCheckDNS checks DNS propagation before notifying ACME that
// the DNS challenge is ready.
PreCheckDNS preCheckDNSFunc = checkDNSPropagation
fqdnToZone = map[string]string{}
PreCheckDNS preCheckDNSFunc = checkDNSPropagation
fqdnToZone = map[string]string{}
muFqdnToZone sync.Mutex
)
const defaultResolvConf = "/etc/resolv.conf"
@ -262,6 +264,9 @@ func lookupNameservers(fqdn string) ([]string, error) {
// FindZoneByFqdn determines the zone apex for the given fqdn by recursing up the
// domain labels until the nameserver returns a SOA record in the answer section.
func FindZoneByFqdn(fqdn string, nameservers []string) (string, error) {
muFqdnToZone.Lock()
defer muFqdnToZone.Unlock()
// Do we have it cached?
if zone, ok := fqdnToZone[fqdn]; ok {
return zone, nil

View file

@ -1,3 +1,4 @@
// Package auroradns implements a DNS provider for solving the DNS-01 challenge using Aurora DNS.
package auroradns
import (
@ -6,9 +7,7 @@ import (
"sync"
"time"
"github.com/edeckers/auroradnsclient"
"github.com/edeckers/auroradnsclient/records"
"github.com/edeckers/auroradnsclient/zones"
"github.com/ldez/go-auroradns"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/platform/config/env"
)
@ -39,7 +38,7 @@ type DNSProvider struct {
recordIDs map[string]string
recordIDsMu sync.Mutex
config *Config
client *auroradnsclient.AuroraDNSClient
client *auroradns.Client
}
// NewDNSProvider returns a DNSProvider instance configured for AuroraDNS.
@ -85,7 +84,12 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
config.BaseURL = defaultBaseURL
}
client, err := auroradnsclient.NewAuroraDNSClient(config.BaseURL, config.UserID, config.Key)
tr, err := auroradns.NewTokenTransport(config.UserID, config.Key)
if err != nil {
return nil, fmt.Errorf("aurora: %v", err)
}
client, err := auroradns.NewClient(tr.Client(), auroradns.WithBaseURL(config.BaseURL))
if err != nil {
return nil, fmt.Errorf("aurora: %v", err)
}
@ -117,26 +121,25 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
authZone = acme.UnFqdn(authZone)
zoneRecord, err := d.getZoneInformationByName(authZone)
zone, err := d.getZoneInformationByName(authZone)
if err != nil {
return fmt.Errorf("aurora: could not create record: %v", err)
}
reqData :=
records.CreateRecordRequest{
RecordType: "TXT",
Name: subdomain,
Content: value,
TTL: d.config.TTL,
}
record := auroradns.Record{
RecordType: "TXT",
Name: subdomain,
Content: value,
TTL: d.config.TTL,
}
respData, err := d.client.CreateRecord(zoneRecord.ID, reqData)
newRecord, _, err := d.client.CreateRecord(zone.ID, record)
if err != nil {
return fmt.Errorf("aurora: could not create record: %v", err)
}
d.recordIDsMu.Lock()
d.recordIDs[fqdn] = respData.ID
d.recordIDs[fqdn] = newRecord.ID
d.recordIDsMu.Unlock()
return nil
@ -161,12 +164,12 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
authZone = acme.UnFqdn(authZone)
zoneRecord, err := d.getZoneInformationByName(authZone)
zone, err := d.getZoneInformationByName(authZone)
if err != nil {
return err
}
_, err = d.client.RemoveRecord(zoneRecord.ID, recordID)
_, _, err = d.client.DeleteRecord(zone.ID, recordID)
if err != nil {
return err
}
@ -184,10 +187,10 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
func (d *DNSProvider) getZoneInformationByName(name string) (zones.ZoneRecord, error) {
zs, err := d.client.GetZones()
func (d *DNSProvider) getZoneInformationByName(name string) (auroradns.Zone, error) {
zs, _, err := d.client.ListZones()
if err != nil {
return zones.ZoneRecord{}, err
return auroradns.Zone{}, err
}
for _, element := range zs {
@ -196,5 +199,5 @@ func (d *DNSProvider) getZoneInformationByName(name string) (zones.ZoneRecord, e
}
}
return zones.ZoneRecord{}, fmt.Errorf("could not find Zone record")
return auroradns.Zone{}, fmt.Errorf("could not find Zone record")
}

View file

@ -7,6 +7,7 @@ import (
"context"
"errors"
"fmt"
"io/ioutil"
"net/http"
"strings"
"time"
@ -15,18 +16,26 @@ import (
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/go-autorest/autorest/azure/auth"
"github.com/Azure/go-autorest/autorest/to"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/platform/config/env"
)
const defaultMetadataEndpoint = "http://169.254.169.254"
// Config is used to configure the creation of the DNSProvider
type Config struct {
ClientID string
ClientSecret string
SubscriptionID string
TenantID string
ResourceGroup string
// optional if using instance metadata service
ClientID string
ClientSecret string
TenantID string
SubscriptionID string
ResourceGroup string
MetadataEndpoint string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
@ -39,29 +48,26 @@ func NewDefaultConfig() *Config {
TTL: env.GetOrDefaultInt("AZURE_TTL", 60),
PropagationTimeout: env.GetOrDefaultSecond("AZURE_PROPAGATION_TIMEOUT", 2*time.Minute),
PollingInterval: env.GetOrDefaultSecond("AZURE_POLLING_INTERVAL", 2*time.Second),
MetadataEndpoint: env.GetOrFile("AZURE_METADATA_ENDPOINT"),
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface
type DNSProvider struct {
config *Config
config *Config
authorizer autorest.Authorizer
}
// NewDNSProvider returns a DNSProvider instance configured for azure.
// Credentials must be passed in the environment variables: AZURE_CLIENT_ID,
// AZURE_CLIENT_SECRET, AZURE_SUBSCRIPTION_ID, AZURE_TENANT_ID, AZURE_RESOURCE_GROUP
// Credentials can be passed in the environment variables:
// AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_SUBSCRIPTION_ID, AZURE_TENANT_ID, AZURE_RESOURCE_GROUP
// If the credentials are _not_ set via the environment,
// then it will attempt to get a bearer token via the instance metadata service.
// see: https://github.com/Azure/go-autorest/blob/v10.14.0/autorest/azure/auth/auth.go#L38-L42
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET", "AZURE_SUBSCRIPTION_ID", "AZURE_TENANT_ID", "AZURE_RESOURCE_GROUP")
if err != nil {
return nil, fmt.Errorf("azure: %v", err)
}
config := NewDefaultConfig()
config.ClientID = values["AZURE_CLIENT_ID"]
config.ClientSecret = values["AZURE_CLIENT_SECRET"]
config.SubscriptionID = values["AZURE_SUBSCRIPTION_ID"]
config.TenantID = values["AZURE_TENANT_ID"]
config.ResourceGroup = values["AZURE_RESOURCE_GROUP"]
config.SubscriptionID = env.GetOrFile("AZURE_SUBSCRIPTION_ID")
config.ResourceGroup = env.GetOrFile("AZURE_RESOURCE_GROUP")
return NewDNSProviderConfig(config)
}
@ -73,8 +79,8 @@ func NewDNSProviderCredentials(clientID, clientSecret, subscriptionID, tenantID,
config := NewDefaultConfig()
config.ClientID = clientID
config.ClientSecret = clientSecret
config.SubscriptionID = subscriptionID
config.TenantID = tenantID
config.SubscriptionID = subscriptionID
config.ResourceGroup = resourceGroup
return NewDNSProviderConfig(config)
@ -86,11 +92,40 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
return nil, errors.New("azure: the configuration of the DNS provider is nil")
}
if config.ClientID == "" || config.ClientSecret == "" || config.SubscriptionID == "" || config.TenantID == "" || config.ResourceGroup == "" {
return nil, errors.New("azure: some credentials information are missing")
if config.HTTPClient == nil {
config.HTTPClient = http.DefaultClient
}
return &DNSProvider{config: config}, nil
authorizer, err := getAuthorizer(config)
if err != nil {
return nil, err
}
if config.SubscriptionID == "" {
subsID, err := getMetadata(config, "subscriptionId")
if err != nil {
return nil, fmt.Errorf("azure: %v", err)
}
if subsID == "" {
return nil, errors.New("azure: SubscriptionID is missing")
}
config.SubscriptionID = subsID
}
if config.ResourceGroup == "" {
resGroup, err := getMetadata(config, "resourceGroupName")
if err != nil {
return nil, fmt.Errorf("azure: %v", err)
}
if resGroup == "" {
return nil, errors.New("azure: ResourceGroup is missing")
}
config.ResourceGroup = resGroup
}
return &DNSProvider{config: config, authorizer: authorizer}, nil
}
// Timeout returns the timeout and interval to use when checking for DNS
@ -110,12 +145,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
}
rsc := dns.NewRecordSetsClient(d.config.SubscriptionID)
spt, err := d.newServicePrincipalToken(azure.PublicCloud.ResourceManagerEndpoint)
if err != nil {
return fmt.Errorf("azure: %v", err)
}
rsc.Authorizer = autorest.NewBearerAuthorizer(spt)
rsc.Authorizer = d.authorizer
relative := toRelativeRecord(fqdn, acme.ToFqdn(zone))
rec := dns.RecordSet{
@ -145,12 +175,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
relative := toRelativeRecord(fqdn, acme.ToFqdn(zone))
rsc := dns.NewRecordSetsClient(d.config.SubscriptionID)
spt, err := d.newServicePrincipalToken(azure.PublicCloud.ResourceManagerEndpoint)
if err != nil {
return fmt.Errorf("azure: %v", err)
}
rsc.Authorizer = autorest.NewBearerAuthorizer(spt)
rsc.Authorizer = d.authorizer
_, err = rsc.Delete(ctx, d.config.ResourceGroup, zone, relative, dns.TXT, "")
if err != nil {
@ -166,14 +191,8 @@ func (d *DNSProvider) getHostedZoneID(ctx context.Context, fqdn string) (string,
return "", err
}
// Now we want to to Azure and get the zone.
spt, err := d.newServicePrincipalToken(azure.PublicCloud.ResourceManagerEndpoint)
if err != nil {
return "", err
}
dc := dns.NewZonesClient(d.config.SubscriptionID)
dc.Authorizer = autorest.NewBearerAuthorizer(spt)
dc.Authorizer = d.authorizer
zone, err := dc.Get(ctx, d.config.ResourceGroup, acme.UnFqdn(authZone))
if err != nil {
@ -184,17 +203,61 @@ func (d *DNSProvider) getHostedZoneID(ctx context.Context, fqdn string) (string,
return to.String(zone.Name), nil
}
// NewServicePrincipalTokenFromCredentials creates a new ServicePrincipalToken using values of the
// passed credentials map.
func (d *DNSProvider) newServicePrincipalToken(scope string) (*adal.ServicePrincipalToken, error) {
oauthConfig, err := adal.NewOAuthConfig(azure.PublicCloud.ActiveDirectoryEndpoint, d.config.TenantID)
if err != nil {
return nil, err
}
return adal.NewServicePrincipalToken(*oauthConfig, d.config.ClientID, d.config.ClientSecret, scope)
}
// Returns the relative record to the domain
func toRelativeRecord(domain, zone string) string {
return acme.UnFqdn(strings.TrimSuffix(domain, zone))
}
func getAuthorizer(config *Config) (autorest.Authorizer, error) {
if config.ClientID != "" && config.ClientSecret != "" && config.TenantID != "" {
oauthConfig, err := adal.NewOAuthConfig(azure.PublicCloud.ActiveDirectoryEndpoint, config.TenantID)
if err != nil {
return nil, err
}
spt, err := adal.NewServicePrincipalToken(*oauthConfig, config.ClientID, config.ClientSecret, azure.PublicCloud.ResourceManagerEndpoint)
if err != nil {
return nil, err
}
spt.SetSender(config.HTTPClient)
return autorest.NewBearerAuthorizer(spt), nil
}
return auth.NewAuthorizerFromEnvironment()
}
// Fetches metadata from environment or he instance metadata service
// borrowed from https://github.com/Microsoft/azureimds/blob/master/imdssample.go
func getMetadata(config *Config, field string) (string, error) {
metadataEndpoint := config.MetadataEndpoint
if len(metadataEndpoint) == 0 {
metadataEndpoint = defaultMetadataEndpoint
}
resource := fmt.Sprintf("%s/metadata/instance/compute/%s", metadataEndpoint, field)
req, err := http.NewRequest(http.MethodGet, resource, nil)
if err != nil {
return "", err
}
req.Header.Add("Metadata", "True")
q := req.URL.Query()
q.Add("format", "text")
q.Add("api-version", "2017-12-01")
req.URL.RawQuery = q.Encode()
resp, err := config.HTTPClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(respBody[:]), nil
}

View file

@ -7,6 +7,7 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
)
@ -27,6 +28,10 @@ type Record struct {
SourceID int `json:"sourceId"`
}
type recordsResponse struct {
Records *[]Record `json:"data"`
}
// Client DNSMadeEasy client
type Client struct {
apiKey string
@ -82,10 +87,6 @@ func (c *Client) GetRecords(domain *Domain, recordName, recordType string) (*[]R
}
defer resp.Body.Close()
type recordsResponse struct {
Records *[]Record `json:"data"`
}
records := &recordsResponse{}
err = json.NewDecoder(resp.Body).Decode(&records)
if err != nil {
@ -151,7 +152,11 @@ func (c *Client) sendRequest(method, resource string, payload interface{}) (*htt
}
if resp.StatusCode > 299 {
return nil, fmt.Errorf("DNSMadeEasy API request failed with HTTP status code %d", resp.StatusCode)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("request failed with HTTP status code %d", resp.StatusCode)
}
return nil, fmt.Errorf("request failed with HTTP status code %d: %s", resp.StatusCode, string(body))
}
return resp, nil

View file

@ -1,3 +1,4 @@
// Package dnsmadeeasy implements a DNS provider for solving the DNS-01 challenge using DNS Made Easy.
package dnsmadeeasy
import (
@ -112,13 +113,13 @@ func (d *DNSProvider) Present(domainName, token, keyAuth string) error {
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
return err
return fmt.Errorf("dnsmadeeasy: unable to find zone for %s: %v", fqdn, err)
}
// fetch the domain details
domain, err := d.client.GetDomain(authZone)
if err != nil {
return err
return fmt.Errorf("dnsmadeeasy: unable to get domain for zone %s: %v", authZone, err)
}
// create the TXT record
@ -126,7 +127,10 @@ func (d *DNSProvider) Present(domainName, token, keyAuth string) error {
record := &Record{Type: "TXT", Name: name, Value: value, TTL: d.config.TTL}
err = d.client.CreateRecord(domain, record)
return err
if err != nil {
return fmt.Errorf("dnsmadeeasy: unable to create record for %s: %v", name, err)
}
return nil
}
// CleanUp removes the TXT records matching the specified parameters
@ -135,31 +139,32 @@ func (d *DNSProvider) CleanUp(domainName, token, keyAuth string) error {
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
return err
return fmt.Errorf("dnsmadeeasy: unable to find zone for %s: %v", fqdn, err)
}
// fetch the domain details
domain, err := d.client.GetDomain(authZone)
if err != nil {
return err
return fmt.Errorf("dnsmadeeasy: unable to get domain for zone %s: %v", authZone, err)
}
// find matching records
name := strings.Replace(fqdn, "."+authZone, "", 1)
records, err := d.client.GetRecords(domain, name, "TXT")
if err != nil {
return err
return fmt.Errorf("dnsmadeeasy: unable to get records for domain %s: %v", domain.Name, err)
}
// delete records
var lastError error
for _, record := range *records {
err = d.client.DeleteRecord(record)
if err != nil {
return err
lastError = fmt.Errorf("dnsmadeeasy: unable to delete record [id=%d, name=%s]: %v", record.ID, record.Name, err)
}
}
return nil
return lastError
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.

View file

@ -1,4 +1,4 @@
// Package dreamhost Adds lego support for http://dreamhost.com DNS updates
// Package dreamhost implements a DNS provider for solving the DNS-01 challenge using DreamHost.
// See https://help.dreamhost.com/hc/en-us/articles/217560167-API_overview
// and https://help.dreamhost.com/hc/en-us/articles/217555707-DNS-API-commands for the API spec.
package dreamhost

View file

@ -1,4 +1,4 @@
// Package duckdns Adds lego support for http://duckdns.org.
// Package duckdns implements a DNS provider for solving the DNS-01 challenge using DuckDNS.
// See http://www.duckdns.org/spec.jsp for more info on updating TXT records.
package duckdns
@ -7,8 +7,12 @@ import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/miekg/dns"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/platform/config/env"
)
@ -96,9 +100,16 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
// To update the TXT record we just need to make one simple get request.
// In DuckDNS you only have one TXT record shared with the domain and all sub domains.
func updateTxtRecord(domain, token, txt string, clear bool) error {
u := fmt.Sprintf("https://www.duckdns.org/update?domains=%s&token=%s&clear=%t&txt=%s", domain, token, clear, txt)
u, _ := url.Parse("https://www.duckdns.org/update")
response, err := acme.HTTPClient.Get(u)
query := u.Query()
query.Set("domains", getMainDomain(domain))
query.Set("token", token)
query.Set("clear", strconv.FormatBool(clear))
query.Set("txt", txt)
u.RawQuery = query.Encode()
response, err := acme.HTTPClient.Get(u.String())
if err != nil {
return err
}
@ -115,3 +126,23 @@ func updateTxtRecord(domain, token, txt string, clear bool) error {
}
return nil
}
// DuckDNS only lets you write to your subdomain
// so it must be in format subdomain.duckdns.org
// not in format subsubdomain.subdomain.duckdns.org
// so strip off everything that is not top 3 levels
func getMainDomain(domain string) string {
domain = acme.UnFqdn(domain)
split := dns.Split(domain)
if strings.HasSuffix(strings.ToLower(domain), "duckdns.org") {
if len(split) < 3 {
return ""
}
firstSubDomainIndex := split[len(split)-3]
return domain[firstSubDomainIndex:]
}
return domain[split[len(split)-1]:]
}

View file

@ -25,27 +25,27 @@ type Request struct {
Param interface{} `json:"param"`
}
// LoginMsg as specified in netcup WSDL
// LoginRequest as specified in netcup WSDL
// https://ccp.netcup.net/run/webservice/servers/endpoint.php#login
type LoginMsg struct {
type LoginRequest struct {
CustomerNumber string `json:"customernumber"`
APIKey string `json:"apikey"`
APIPassword string `json:"apipassword"`
ClientRequestID string `json:"clientrequestid,omitempty"`
}
// LogoutMsg as specified in netcup WSDL
// LogoutRequest as specified in netcup WSDL
// https://ccp.netcup.net/run/webservice/servers/endpoint.php#logout
type LogoutMsg struct {
type LogoutRequest struct {
CustomerNumber string `json:"customernumber"`
APIKey string `json:"apikey"`
APISessionID string `json:"apisessionid"`
ClientRequestID string `json:"clientrequestid,omitempty"`
}
// UpdateDNSRecordsMsg as specified in netcup WSDL
// UpdateDNSRecordsRequest as specified in netcup WSDL
// https://ccp.netcup.net/run/webservice/servers/endpoint.php#updateDnsRecords
type UpdateDNSRecordsMsg struct {
type UpdateDNSRecordsRequest struct {
DomainName string `json:"domainname"`
CustomerNumber string `json:"customernumber"`
APIKey string `json:"apikey"`
@ -55,15 +55,15 @@ type UpdateDNSRecordsMsg struct {
}
// DNSRecordSet as specified in netcup WSDL
// needed in UpdateDNSRecordsMsg
// needed in UpdateDNSRecordsRequest
// https://ccp.netcup.net/run/webservice/servers/endpoint.php#Dnsrecordset
type DNSRecordSet struct {
DNSRecords []DNSRecord `json:"dnsrecords"`
}
// InfoDNSRecordsMsg as specified in netcup WSDL
// InfoDNSRecordsRequest as specified in netcup WSDL
// https://ccp.netcup.net/run/webservice/servers/endpoint.php#infoDnsRecords
type InfoDNSRecordsMsg struct {
type InfoDNSRecordsRequest struct {
DomainName string `json:"domainname"`
CustomerNumber string `json:"customernumber"`
APIKey string `json:"apikey"`
@ -87,33 +87,30 @@ type DNSRecord struct {
// ResponseMsg as specified in netcup WSDL
// https://ccp.netcup.net/run/webservice/servers/endpoint.php#Responsemessage
type ResponseMsg struct {
ServerRequestID string `json:"serverrequestid"`
ClientRequestID string `json:"clientrequestid,omitempty"`
Action string `json:"action"`
Status string `json:"status"`
StatusCode int `json:"statuscode"`
ShortMessage string `json:"shortmessage"`
LongMessage string `json:"longmessage"`
ResponseData ResponseData `json:"responsedata,omitempty"`
ServerRequestID string `json:"serverrequestid"`
ClientRequestID string `json:"clientrequestid,omitempty"`
Action string `json:"action"`
Status string `json:"status"`
StatusCode int `json:"statuscode"`
ShortMessage string `json:"shortmessage"`
LongMessage string `json:"longmessage"`
ResponseData json.RawMessage `json:"responsedata,omitempty"`
}
// LogoutResponseMsg similar to ResponseMsg
// allows empty ResponseData field whilst unmarshaling
type LogoutResponseMsg struct {
ServerRequestID string `json:"serverrequestid"`
ClientRequestID string `json:"clientrequestid,omitempty"`
Action string `json:"action"`
Status string `json:"status"`
StatusCode int `json:"statuscode"`
ShortMessage string `json:"shortmessage"`
LongMessage string `json:"longmessage"`
ResponseData string `json:"responsedata,omitempty"`
func (r *ResponseMsg) Error() string {
return fmt.Sprintf("an error occurred during the action %s: [Status=%s, StatusCode=%d, ShortMessage=%s, LongMessage=%s]",
r.Action, r.Status, r.StatusCode, r.ShortMessage, r.LongMessage)
}
// ResponseData to enable correct unmarshaling of ResponseMsg
type ResponseData struct {
// LoginResponse response to login action.
type LoginResponse struct {
APISessionID string `json:"apisessionid"`
}
// InfoDNSRecordsResponse response to infoDnsRecords action.
type InfoDNSRecordsResponse struct {
APISessionID string `json:"apisessionid"`
DNSRecords []DNSRecord `json:"dnsrecords"`
DNSRecords []DNSRecord `json:"dnsrecords,omitempty"`
}
// Client netcup DNS client
@ -126,7 +123,11 @@ type Client struct {
}
// NewClient creates a netcup DNS client
func NewClient(customerNumber string, apiKey string, apiPassword string) *Client {
func NewClient(customerNumber string, apiKey string, apiPassword string) (*Client, error) {
if customerNumber == "" || apiKey == "" || apiPassword == "" {
return nil, fmt.Errorf("credentials missing")
}
return &Client{
customerNumber: customerNumber,
apiKey: apiKey,
@ -135,7 +136,7 @@ func NewClient(customerNumber string, apiKey string, apiPassword string) *Client
HTTPClient: &http.Client{
Timeout: 10 * time.Second,
},
}
}, nil
}
// Login performs the login as specified by the netcup WSDL
@ -144,7 +145,7 @@ func NewClient(customerNumber string, apiKey string, apiPassword string) *Client
func (c *Client) Login() (string, error) {
payload := &Request{
Action: "login",
Param: &LoginMsg{
Param: &LoginRequest{
CustomerNumber: c.customerNumber,
APIKey: c.apiKey,
APIPassword: c.apiPassword,
@ -152,21 +153,13 @@ func (c *Client) Login() (string, error) {
},
}
response, err := c.sendRequest(payload)
var responseData LoginResponse
err := c.doRequest(payload, &responseData)
if err != nil {
return "", fmt.Errorf("error sending request to DNS-API, %v", err)
return "", fmt.Errorf("loging error: %v", err)
}
var r ResponseMsg
err = json.Unmarshal(response, &r)
if err != nil {
return "", fmt.Errorf("error decoding response of DNS-API, %v", err)
}
if r.Status != success {
return "", fmt.Errorf("error logging into DNS-API, %v", r.LongMessage)
}
return r.ResponseData.APISessionID, nil
return responseData.APISessionID, nil
}
// Logout performs the logout with the supplied sessionID as specified by the netcup WSDL
@ -174,7 +167,7 @@ func (c *Client) Login() (string, error) {
func (c *Client) Logout(sessionID string) error {
payload := &Request{
Action: "logout",
Param: &LogoutMsg{
Param: &LogoutRequest{
CustomerNumber: c.customerNumber,
APIKey: c.apiKey,
APISessionID: sessionID,
@ -182,54 +175,34 @@ func (c *Client) Logout(sessionID string) error {
},
}
response, err := c.sendRequest(payload)
err := c.doRequest(payload, nil)
if err != nil {
return fmt.Errorf("error logging out of DNS-API: %v", err)
return fmt.Errorf("logout error: %v", err)
}
var r LogoutResponseMsg
err = json.Unmarshal(response, &r)
if err != nil {
return fmt.Errorf("error logging out of DNS-API: %v", err)
}
if r.Status != success {
return fmt.Errorf("error logging out of DNS-API: %v", r.ShortMessage)
}
return nil
}
// UpdateDNSRecord performs an update of the DNSRecords as specified by the netcup WSDL
// https://ccp.netcup.net/run/webservice/servers/endpoint.php
func (c *Client) UpdateDNSRecord(sessionID, domainName string, record DNSRecord) error {
func (c *Client) UpdateDNSRecord(sessionID, domainName string, records []DNSRecord) error {
payload := &Request{
Action: "updateDnsRecords",
Param: UpdateDNSRecordsMsg{
Param: UpdateDNSRecordsRequest{
DomainName: domainName,
CustomerNumber: c.customerNumber,
APIKey: c.apiKey,
APISessionID: sessionID,
ClientRequestID: "",
DNSRecordSet: DNSRecordSet{DNSRecords: []DNSRecord{record}},
DNSRecordSet: DNSRecordSet{DNSRecords: records},
},
}
response, err := c.sendRequest(payload)
err := c.doRequest(payload, nil)
if err != nil {
return err
return fmt.Errorf("error when sending the request: %v", err)
}
var r ResponseMsg
err = json.Unmarshal(response, &r)
if err != nil {
return err
}
if r.Status != success {
return fmt.Errorf("%s: %+v", r.ShortMessage, r)
}
return nil
}
@ -239,7 +212,7 @@ func (c *Client) UpdateDNSRecord(sessionID, domainName string, record DNSRecord)
func (c *Client) GetDNSRecords(hostname, apiSessionID string) ([]DNSRecord, error) {
payload := &Request{
Action: "infoDnsRecords",
Param: InfoDNSRecordsMsg{
Param: InfoDNSRecordsRequest{
DomainName: hostname,
CustomerNumber: c.customerNumber,
APIKey: c.apiKey,
@ -248,82 +221,98 @@ func (c *Client) GetDNSRecords(hostname, apiSessionID string) ([]DNSRecord, erro
},
}
response, err := c.sendRequest(payload)
var responseData InfoDNSRecordsResponse
err := c.doRequest(payload, &responseData)
if err != nil {
return nil, err
return nil, fmt.Errorf("error when sending the request: %v", err)
}
var r ResponseMsg
err = json.Unmarshal(response, &r)
if err != nil {
return nil, err
}
if r.Status != success {
return nil, fmt.Errorf("%s", r.ShortMessage)
}
return r.ResponseData.DNSRecords, nil
return responseData.DNSRecords, nil
}
// sendRequest marshals given body to JSON, send the request to netcup API
// doRequest marshals given body to JSON, send the request to netcup API
// and returns body of response
func (c *Client) sendRequest(payload interface{}) ([]byte, error) {
func (c *Client) doRequest(payload interface{}, responseData interface{}) error {
body, err := json.Marshal(payload)
if err != nil {
return nil, err
return err
}
req, err := http.NewRequest(http.MethodPost, c.BaseURL, bytes.NewReader(body))
if err != nil {
return nil, err
return err
}
req.Close = true
req.Close = true
req.Header.Set("content-type", "application/json")
req.Header.Set("User-Agent", acme.UserAgent)
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
return err
}
if resp.StatusCode > 299 {
return nil, fmt.Errorf("API request failed with HTTP Status code %d", resp.StatusCode)
if err = checkResponse(resp); err != nil {
return err
}
body, err = ioutil.ReadAll(resp.Body)
respMsg, err := decodeResponseMsg(resp)
if err != nil {
return nil, fmt.Errorf("read of response body failed, %v", err)
return err
}
defer resp.Body.Close()
return body, nil
}
if respMsg.Status != success {
return respMsg
}
// GetDNSRecordIdx searches a given array of DNSRecords for a given DNSRecord
// equivalence is determined by Destination and RecortType attributes
// returns index of given DNSRecord in given array of DNSRecords
func GetDNSRecordIdx(records []DNSRecord, record DNSRecord) (int, error) {
for index, element := range records {
if record.Destination == element.Destination && record.RecordType == element.RecordType {
return index, nil
if responseData != nil {
err = json.Unmarshal(respMsg.ResponseData, responseData)
if err != nil {
return fmt.Errorf("%v: unmarshaling %T error: %v: %s",
respMsg, responseData, err, string(respMsg.ResponseData))
}
}
return -1, fmt.Errorf("no DNS Record found")
return nil
}
// CreateTxtRecord uses the supplied values to return a DNSRecord of type TXT for the dns-01 challenge
func CreateTxtRecord(hostname, value string, ttl int) DNSRecord {
return DNSRecord{
ID: 0,
Hostname: hostname,
RecordType: "TXT",
Priority: "",
Destination: value,
DeleteRecord: false,
State: "",
TTL: ttl,
func checkResponse(resp *http.Response) error {
if resp.StatusCode > 299 {
if resp.Body == nil {
return fmt.Errorf("response body is nil, status code=%d", resp.StatusCode)
}
defer resp.Body.Close()
raw, err := ioutil.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("unable to read body: status code=%d, error=%v", resp.StatusCode, err)
}
return fmt.Errorf("status code=%d: %s", resp.StatusCode, string(raw))
}
return nil
}
func decodeResponseMsg(resp *http.Response) (*ResponseMsg, error) {
if resp.Body == nil {
return nil, fmt.Errorf("response body is nil, status code=%d", resp.StatusCode)
}
defer resp.Body.Close()
raw, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("unable to read body: status code=%d, error=%v", resp.StatusCode, err)
}
var respMsg ResponseMsg
err = json.Unmarshal(raw, &respMsg)
if err != nil {
return nil, fmt.Errorf("unmarshaling %T error [status code=%d]: %v: %s", respMsg, resp.StatusCode, err, string(raw))
}
return &respMsg, nil
}

View file

@ -9,6 +9,7 @@ import (
"time"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/log"
"github.com/xenolf/lego/platform/config/env"
)
@ -27,8 +28,8 @@ type Config struct {
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("NETCUP_TTL", 120),
PropagationTimeout: env.GetOrDefaultSecond("NETCUP_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("NETCUP_POLLING_INTERVAL", acme.DefaultPollingInterval),
PropagationTimeout: env.GetOrDefaultSecond("NETCUP_PROPAGATION_TIMEOUT", 120*time.Second),
PollingInterval: env.GetOrDefaultSecond("NETCUP_POLLING_INTERVAL", 5*time.Second),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("NETCUP_HTTP_TIMEOUT", 10*time.Second),
},
@ -76,11 +77,11 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
return nil, errors.New("netcup: the configuration of the DNS provider is nil")
}
if config.Customer == "" || config.Key == "" || config.Password == "" {
return nil, fmt.Errorf("netcup: netcup credentials missing")
client, err := NewClient(config.Customer, config.Key, config.Password)
if err != nil {
return nil, fmt.Errorf("netcup: %v", err)
}
client := NewClient(config.Customer, config.Key, config.Password)
client.HTTPClient = config.HTTPClient
return &DNSProvider{client: client, config: config}, nil
@ -100,27 +101,37 @@ func (d *DNSProvider) Present(domainName, token, keyAuth string) error {
return fmt.Errorf("netcup: %v", err)
}
hostname := strings.Replace(fqdn, "."+zone, "", 1)
record := CreateTxtRecord(hostname, value, d.config.TTL)
err = d.client.UpdateDNSRecord(sessionID, acme.UnFqdn(zone), record)
if err != nil {
if errLogout := d.client.Logout(sessionID); errLogout != nil {
return fmt.Errorf("netcup: failed to add TXT-Record: %v; %v", err, errLogout)
defer func() {
err = d.client.Logout(sessionID)
if err != nil {
log.Print("netcup: %v", err)
}
}()
hostname := strings.Replace(fqdn, "."+zone, "", 1)
record := createTxtRecord(hostname, value, d.config.TTL)
zone = acme.UnFqdn(zone)
records, err := d.client.GetDNSRecords(zone, sessionID)
if err != nil {
// skip no existing records
log.Infof("no existing records, error ignored: %v", err)
}
records = append(records, record)
err = d.client.UpdateDNSRecord(sessionID, zone, records)
if err != nil {
return fmt.Errorf("netcup: failed to add TXT-Record: %v", err)
}
err = d.client.Logout(sessionID)
if err != nil {
return fmt.Errorf("netcup: %v", err)
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domainname, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domainname, keyAuth)
func (d *DNSProvider) CleanUp(domainName, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domainName, keyAuth)
zone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
@ -132,6 +143,13 @@ func (d *DNSProvider) CleanUp(domainname, token, keyAuth string) error {
return fmt.Errorf("netcup: %v", err)
}
defer func() {
err = d.client.Logout(sessionID)
if err != nil {
log.Print("netcup: %v", err)
}
}()
hostname := strings.Replace(fqdn, "."+zone, "", 1)
zone = acme.UnFqdn(zone)
@ -141,27 +159,20 @@ func (d *DNSProvider) CleanUp(domainname, token, keyAuth string) error {
return fmt.Errorf("netcup: %v", err)
}
record := CreateTxtRecord(hostname, value, 0)
record := createTxtRecord(hostname, value, 0)
idx, err := GetDNSRecordIdx(records, record)
idx, err := getDNSRecordIdx(records, record)
if err != nil {
return fmt.Errorf("netcup: %v", err)
}
records[idx].DeleteRecord = true
err = d.client.UpdateDNSRecord(sessionID, zone, records[idx])
err = d.client.UpdateDNSRecord(sessionID, zone, []DNSRecord{records[idx]})
if err != nil {
if errLogout := d.client.Logout(sessionID); errLogout != nil {
return fmt.Errorf("netcup: %v; %v", err, errLogout)
}
return fmt.Errorf("netcup: %v", err)
}
err = d.client.Logout(sessionID)
if err != nil {
return fmt.Errorf("netcup: %v", err)
}
return nil
}
@ -170,3 +181,29 @@ func (d *DNSProvider) CleanUp(domainname, token, keyAuth string) error {
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
// getDNSRecordIdx searches a given array of DNSRecords for a given DNSRecord
// equivalence is determined by Destination and RecortType attributes
// returns index of given DNSRecord in given array of DNSRecords
func getDNSRecordIdx(records []DNSRecord, record DNSRecord) (int, error) {
for index, element := range records {
if record.Destination == element.Destination && record.RecordType == element.RecordType {
return index, nil
}
}
return -1, fmt.Errorf("no DNS Record found")
}
// createTxtRecord uses the supplied values to return a DNSRecord of type TXT for the dns-01 challenge
func createTxtRecord(hostname, value string, ttl int) DNSRecord {
return DNSRecord{
ID: 0,
Hostname: hostname,
RecordType: "TXT",
Priority: "",
Destination: value,
DeleteRecord: false,
State: "",
TTL: ttl,
}
}