forked from TrueCloudLab/lego
Add DNS provider for HyperOne (#1233)
This commit is contained in:
parent
d14bef50f3
commit
7557dbc98c
21 changed files with 1440 additions and 10 deletions
20
README.md
20
README.md
|
@ -55,15 +55,15 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns).
|
|||
| [EasyDNS](https://go-acme.github.io/lego/dns/easydns/) | [Exoscale](https://go-acme.github.io/lego/dns/exoscale/) | [External program](https://go-acme.github.io/lego/dns/exec/) | [FastDNS (Deprecated)](https://go-acme.github.io/lego/dns/fastdns/) |
|
||||
| [Gandi Live DNS (v5)](https://go-acme.github.io/lego/dns/gandiv5/) | [Gandi](https://go-acme.github.io/lego/dns/gandi/) | [Glesys](https://go-acme.github.io/lego/dns/glesys/) | [Go Daddy](https://go-acme.github.io/lego/dns/godaddy/) |
|
||||
| [Google Cloud](https://go-acme.github.io/lego/dns/gcloud/) | [Hetzner](https://go-acme.github.io/lego/dns/hetzner/) | [Hosting.de](https://go-acme.github.io/lego/dns/hostingde/) | [HTTP request](https://go-acme.github.io/lego/dns/httpreq/) |
|
||||
| [Internet Initiative Japan](https://go-acme.github.io/lego/dns/iij/) | [INWX](https://go-acme.github.io/lego/dns/inwx/) | [Joker](https://go-acme.github.io/lego/dns/joker/) | [Joohoi's ACME-DNS](https://go-acme.github.io/lego/dns/acme-dns/) |
|
||||
| [Linode (deprecated)](https://go-acme.github.io/lego/dns/linode/) | [Linode (v4)](https://go-acme.github.io/lego/dns/linodev4/) | [Liquid Web](https://go-acme.github.io/lego/dns/liquidweb/) | [LuaDNS](https://go-acme.github.io/lego/dns/luadns/) |
|
||||
| [Manual](https://go-acme.github.io/lego/dns/manual/) | [MyDNS.jp](https://go-acme.github.io/lego/dns/mydnsjp/) | [MythicBeasts](https://go-acme.github.io/lego/dns/mythicbeasts/) | [Name.com](https://go-acme.github.io/lego/dns/namedotcom/) |
|
||||
| [Namecheap](https://go-acme.github.io/lego/dns/namecheap/) | [Namesilo](https://go-acme.github.io/lego/dns/namesilo/) | [Netcup](https://go-acme.github.io/lego/dns/netcup/) | [Netlify](https://go-acme.github.io/lego/dns/netlify/) |
|
||||
| [NIFCloud](https://go-acme.github.io/lego/dns/nifcloud/) | [NS1](https://go-acme.github.io/lego/dns/ns1/) | [Open Telekom Cloud](https://go-acme.github.io/lego/dns/otc/) | [Oracle Cloud](https://go-acme.github.io/lego/dns/oraclecloud/) |
|
||||
| [OVH](https://go-acme.github.io/lego/dns/ovh/) | [PowerDNS](https://go-acme.github.io/lego/dns/pdns/) | [Rackspace](https://go-acme.github.io/lego/dns/rackspace/) | [reg.ru](https://go-acme.github.io/lego/dns/regru/) |
|
||||
| [RFC2136](https://go-acme.github.io/lego/dns/rfc2136/) | [RimuHosting](https://go-acme.github.io/lego/dns/rimuhosting/) | [Sakura Cloud](https://go-acme.github.io/lego/dns/sakuracloud/) | [Scaleway](https://go-acme.github.io/lego/dns/scaleway/) |
|
||||
| [Selectel](https://go-acme.github.io/lego/dns/selectel/) | [Servercow](https://go-acme.github.io/lego/dns/servercow/) | [Stackpath](https://go-acme.github.io/lego/dns/stackpath/) | [TransIP](https://go-acme.github.io/lego/dns/transip/) |
|
||||
| [VegaDNS](https://go-acme.github.io/lego/dns/vegadns/) | [Versio.[nl/eu/uk]](https://go-acme.github.io/lego/dns/versio/) | [Vscale](https://go-acme.github.io/lego/dns/vscale/) | [Vultr](https://go-acme.github.io/lego/dns/vultr/) |
|
||||
| [Yandex](https://go-acme.github.io/lego/dns/yandex/) | [Zone.ee](https://go-acme.github.io/lego/dns/zoneee/) | [Zonomi](https://go-acme.github.io/lego/dns/zonomi/) | |
|
||||
| [HyperOne](https://go-acme.github.io/lego/dns/hyperone/) | [Internet Initiative Japan](https://go-acme.github.io/lego/dns/iij/) | [INWX](https://go-acme.github.io/lego/dns/inwx/) | [Joker](https://go-acme.github.io/lego/dns/joker/) |
|
||||
| [Joohoi's ACME-DNS](https://go-acme.github.io/lego/dns/acme-dns/) | [Linode (deprecated)](https://go-acme.github.io/lego/dns/linode/) | [Linode (v4)](https://go-acme.github.io/lego/dns/linodev4/) | [Liquid Web](https://go-acme.github.io/lego/dns/liquidweb/) |
|
||||
| [LuaDNS](https://go-acme.github.io/lego/dns/luadns/) | [Manual](https://go-acme.github.io/lego/dns/manual/) | [MyDNS.jp](https://go-acme.github.io/lego/dns/mydnsjp/) | [MythicBeasts](https://go-acme.github.io/lego/dns/mythicbeasts/) |
|
||||
| [Name.com](https://go-acme.github.io/lego/dns/namedotcom/) | [Namecheap](https://go-acme.github.io/lego/dns/namecheap/) | [Namesilo](https://go-acme.github.io/lego/dns/namesilo/) | [Netcup](https://go-acme.github.io/lego/dns/netcup/) |
|
||||
| [Netlify](https://go-acme.github.io/lego/dns/netlify/) | [NIFCloud](https://go-acme.github.io/lego/dns/nifcloud/) | [NS1](https://go-acme.github.io/lego/dns/ns1/) | [Open Telekom Cloud](https://go-acme.github.io/lego/dns/otc/) |
|
||||
| [Oracle Cloud](https://go-acme.github.io/lego/dns/oraclecloud/) | [OVH](https://go-acme.github.io/lego/dns/ovh/) | [PowerDNS](https://go-acme.github.io/lego/dns/pdns/) | [Rackspace](https://go-acme.github.io/lego/dns/rackspace/) |
|
||||
| [reg.ru](https://go-acme.github.io/lego/dns/regru/) | [RFC2136](https://go-acme.github.io/lego/dns/rfc2136/) | [RimuHosting](https://go-acme.github.io/lego/dns/rimuhosting/) | [Sakura Cloud](https://go-acme.github.io/lego/dns/sakuracloud/) |
|
||||
| [Scaleway](https://go-acme.github.io/lego/dns/scaleway/) | [Selectel](https://go-acme.github.io/lego/dns/selectel/) | [Servercow](https://go-acme.github.io/lego/dns/servercow/) | [Stackpath](https://go-acme.github.io/lego/dns/stackpath/) |
|
||||
| [TransIP](https://go-acme.github.io/lego/dns/transip/) | [VegaDNS](https://go-acme.github.io/lego/dns/vegadns/) | [Versio.[nl/eu/uk]](https://go-acme.github.io/lego/dns/versio/) | [Vscale](https://go-acme.github.io/lego/dns/vscale/) |
|
||||
| [Vultr](https://go-acme.github.io/lego/dns/vultr/) | [Yandex](https://go-acme.github.io/lego/dns/yandex/) | [Zone.ee](https://go-acme.github.io/lego/dns/zoneee/) | [Zonomi](https://go-acme.github.io/lego/dns/zonomi/) |
|
||||
|
||||
<!-- END DNS PROVIDERS LIST -->
|
||||
|
|
|
@ -53,6 +53,7 @@ func allDNSCodes() string {
|
|||
"hetzner",
|
||||
"hostingde",
|
||||
"httpreq",
|
||||
"hyperone",
|
||||
"iij",
|
||||
"inwx",
|
||||
"joker",
|
||||
|
@ -928,6 +929,24 @@ func displayDNSHelp(name string) error {
|
|||
ew.writeln()
|
||||
ew.writeln(`More information: https://go-acme.github.io/lego/dns/httpreq`)
|
||||
|
||||
case "hyperone":
|
||||
// generated from: providers/dns/hyperone/hyperone.toml
|
||||
ew.writeln(`Configuration for HyperOne.`)
|
||||
ew.writeln(`Code: 'hyperone'`)
|
||||
ew.writeln(`Since: ''`)
|
||||
ew.writeln()
|
||||
|
||||
ew.writeln(`Additional Configuration:`)
|
||||
ew.writeln(` - "HYPERONE_API_URL": Allows to pass custom API Endpoint to be used in the challenge (default https://api.hyperone.com/v2)`)
|
||||
ew.writeln(` - "HYPERONE_LOCATION_ID": Specifies location (region) to be used in API calls. (default pl-waw-1)`)
|
||||
ew.writeln(` - "HYPERONE_PASSPORT_LOCATION": Allows to pass custom passport file location (default ~/.h1/passport.json)`)
|
||||
ew.writeln(` - "HYPERONE_POLLING_INTERVAL": Time between DNS propagation check`)
|
||||
ew.writeln(` - "HYPERONE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`)
|
||||
ew.writeln(` - "HYPERONE_TTL": The TTL of the TXT record used for the DNS challenge`)
|
||||
|
||||
ew.writeln()
|
||||
ew.writeln(`More information: https://go-acme.github.io/lego/dns/hyperone`)
|
||||
|
||||
case "iij":
|
||||
// generated from: providers/dns/iij/iij.toml
|
||||
ew.writeln(`Configuration for Internet Initiative Japan.`)
|
||||
|
|
78
docs/content/dns/zz_gen_hyperone.md
Normal file
78
docs/content/dns/zz_gen_hyperone.md
Normal file
|
@ -0,0 +1,78 @@
|
|||
---
|
||||
title: "HyperOne"
|
||||
date: 2019-03-03T16:39:46+01:00
|
||||
draft: false
|
||||
slug: hyperone
|
||||
---
|
||||
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
<!-- providers/dns/hyperone/hyperone.toml -->
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
|
||||
Since:
|
||||
|
||||
Configuration for [HyperOne](https://www.hyperone.com).
|
||||
|
||||
|
||||
<!--more-->
|
||||
|
||||
- Code: `hyperone`
|
||||
|
||||
Here is an example bash command using the HyperOne provider:
|
||||
|
||||
```bash
|
||||
lego --dns hyperone --domains my.domain.com --email my@email.com run
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Additional Configuration
|
||||
|
||||
| Environment Variable Name | Description |
|
||||
|--------------------------------|-------------|
|
||||
| `HYPERONE_API_URL` | Allows to pass custom API Endpoint to be used in the challenge (default https://api.hyperone.com/v2) |
|
||||
| `HYPERONE_LOCATION_ID` | Specifies location (region) to be used in API calls. (default pl-waw-1) |
|
||||
| `HYPERONE_PASSPORT_LOCATION` | Allows to pass custom passport file location (default ~/.h1/passport.json) |
|
||||
| `HYPERONE_POLLING_INTERVAL` | Time between DNS propagation check |
|
||||
| `HYPERONE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation |
|
||||
| `HYPERONE_TTL` | The TTL of the TXT record used for the DNS challenge |
|
||||
|
||||
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
|
||||
More information [here](/lego/dns/#configuration-and-credentials).
|
||||
|
||||
## Description
|
||||
|
||||
Default configuration does not require any additional environment variables,
|
||||
just a passport file in `~/.h1/passport.json` location.
|
||||
|
||||
### Generating passport file using H1 CLI
|
||||
|
||||
To use this application you have to generate passport file for `sa`:
|
||||
|
||||
```
|
||||
h1 sa credential generate --name my-passport --sa <sa ID> --passport-output-file ~/.h1/passport.json
|
||||
```
|
||||
|
||||
### Required permissions
|
||||
|
||||
Depending of environment variables usage, the application requires different permissions:
|
||||
- `dns/zone/list` if `HYPERONE_ZONE_URI` is not specified
|
||||
- `dns/zone.recordset/list`
|
||||
- `dns/zone.recordset/create`
|
||||
- `dns/zone.recordset/delete`
|
||||
- `dns/zone.record/create`
|
||||
- `dns/zone.record/list`
|
||||
- `dns/zone.record/delete`
|
||||
|
||||
|
||||
|
||||
## More information
|
||||
|
||||
- [API documentation](https://api.hyperone.com/v2/docs)
|
||||
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
<!-- providers/dns/hyperone/hyperone.toml -->
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
|
@ -44,6 +44,7 @@ import (
|
|||
"github.com/go-acme/lego/v3/providers/dns/hetzner"
|
||||
"github.com/go-acme/lego/v3/providers/dns/hostingde"
|
||||
"github.com/go-acme/lego/v3/providers/dns/httpreq"
|
||||
"github.com/go-acme/lego/v3/providers/dns/hyperone"
|
||||
"github.com/go-acme/lego/v3/providers/dns/iij"
|
||||
"github.com/go-acme/lego/v3/providers/dns/inwx"
|
||||
"github.com/go-acme/lego/v3/providers/dns/joker"
|
||||
|
@ -166,6 +167,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {
|
|||
return hostingde.NewDNSProvider()
|
||||
case "httpreq":
|
||||
return httpreq.NewDNSProvider()
|
||||
case "hyperone":
|
||||
return hyperone.NewDNSProvider()
|
||||
case "iij":
|
||||
return iij.NewDNSProvider()
|
||||
case "inwx":
|
||||
|
|
203
providers/dns/hyperone/hyperone.go
Normal file
203
providers/dns/hyperone/hyperone.go
Normal file
|
@ -0,0 +1,203 @@
|
|||
// Package hyperone implements a DNS provider for solving the DNS-01 challenge using HyperOne.
|
||||
package hyperone
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v3/challenge/dns01"
|
||||
"github.com/go-acme/lego/v3/platform/config/env"
|
||||
"github.com/go-acme/lego/v3/providers/dns/hyperone/internal"
|
||||
)
|
||||
|
||||
// Environment variables names.
|
||||
const (
|
||||
envNamespace = "HYPERONE_"
|
||||
|
||||
EnvPassportLocation = envNamespace + "PASSPORT_LOCATION"
|
||||
EnvAPIUrl = envNamespace + "API_URL"
|
||||
EnvLocationID = envNamespace + "LOCATION_ID"
|
||||
|
||||
EnvTTL = envNamespace + "TTL"
|
||||
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
|
||||
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
|
||||
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
|
||||
)
|
||||
|
||||
// Config is used to configure the creation of the DNSProvider.
|
||||
type Config struct {
|
||||
APIEndpoint string
|
||||
LocationID string
|
||||
PassportLocation string
|
||||
|
||||
TTL int
|
||||
PropagationTimeout time.Duration
|
||||
PollingInterval time.Duration
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// NewDefaultConfig returns a default configuration for the DNSProvider.
|
||||
func NewDefaultConfig() *Config {
|
||||
return &Config{
|
||||
TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
|
||||
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
|
||||
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
|
||||
HTTPClient: &http.Client{
|
||||
Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// DNSProvider implements the challenge.Provider interface.
|
||||
type DNSProvider struct {
|
||||
client *internal.Client
|
||||
config *Config
|
||||
}
|
||||
|
||||
// NewDNSProvider returns a DNSProvider instance configured for HyperOne.
|
||||
func NewDNSProvider() (*DNSProvider, error) {
|
||||
config := NewDefaultConfig()
|
||||
|
||||
config.PassportLocation = env.GetOrFile(EnvPassportLocation)
|
||||
config.LocationID = env.GetOrFile(EnvLocationID)
|
||||
config.APIEndpoint = env.GetOrFile(EnvAPIUrl)
|
||||
|
||||
return NewDNSProviderConfig(config)
|
||||
}
|
||||
|
||||
// NewDNSProviderConfig return a DNSProvider instance configured for HyperOne.
|
||||
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
|
||||
if config.PassportLocation == "" {
|
||||
var err error
|
||||
config.PassportLocation, err = GetDefaultPassportLocation()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("hyperone: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
passport, err := internal.LoadPassportFile(config.PassportLocation)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("hyperone: %w", err)
|
||||
}
|
||||
|
||||
client, err := internal.NewClient(config.APIEndpoint, config.LocationID, passport)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("hyperone: failed to create client: %w", err)
|
||||
}
|
||||
|
||||
if config.HTTPClient != nil {
|
||||
client.HTTPClient = config.HTTPClient
|
||||
}
|
||||
|
||||
return &DNSProvider{client: client, config: config}, nil
|
||||
}
|
||||
|
||||
// Timeout returns the timeout and interval to use when checking for DNS propagation.
|
||||
// Adjusting here to cope with spikes in propagation times.
|
||||
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
||||
return d.config.PropagationTimeout, d.config.PollingInterval
|
||||
}
|
||||
|
||||
// Present creates a TXT record to fulfill the dns-01 challenge.
|
||||
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||
fqdn, value := dns01.GetRecord(domain, keyAuth)
|
||||
|
||||
zone, err := d.getHostedZone(fqdn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("hyperone: failed to get zone for fqdn=%s: %w", fqdn, err)
|
||||
}
|
||||
|
||||
recordset, err := d.client.FindRecordset(zone.ID, "TXT", fqdn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("hyperone: fqdn=%s, zone ID=%s: %w", fqdn, zone.ID, err)
|
||||
}
|
||||
|
||||
if recordset == nil {
|
||||
_, err = d.client.CreateRecordset(zone.ID, "TXT", fqdn, value, d.config.TTL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("hyperone: failed to create recordset: fqdn=%s, zone ID=%s, value=%s: %w", fqdn, zone.ID, value, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = d.client.CreateRecord(zone.ID, recordset.ID, value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("hyperone: failed to create record: fqdn=%s, zone ID=%s, recordset ID=%s: %w", fqdn, zone.ID, recordset.ID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanUp removes the TXT record matching the specified parameters and recordset if no other records are remaining.
|
||||
// There is a small possibility that race will cause to delete recordset with records for other DNS Challenges.
|
||||
func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error {
|
||||
fqdn, value := dns01.GetRecord(domain, keyAuth)
|
||||
|
||||
zone, err := d.getHostedZone(fqdn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("hyperone: failed to get zone for fqdn=%s: %w", fqdn, err)
|
||||
}
|
||||
|
||||
recordset, err := d.client.FindRecordset(zone.ID, "TXT", fqdn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("hyperone: fqdn=%s, zone ID=%s: %w", fqdn, zone.ID, err)
|
||||
}
|
||||
|
||||
if recordset == nil {
|
||||
return fmt.Errorf("hyperone: recordset to remove not found: fqdn=%s", fqdn)
|
||||
}
|
||||
|
||||
records, err := d.client.GetRecords(zone.ID, recordset.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("hyperone: %w", err)
|
||||
}
|
||||
|
||||
if len(records) == 1 {
|
||||
if records[0].Content != value {
|
||||
return fmt.Errorf("hyperone: record with content %s not found: fqdn=%s", value, fqdn)
|
||||
}
|
||||
|
||||
err = d.client.DeleteRecordset(zone.ID, recordset.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("hyperone: failed to delete record: fqdn=%s, zone ID=%s, recordset ID=%s: %w", fqdn, zone.ID, recordset.ID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, record := range records {
|
||||
if record.Content == value {
|
||||
err = d.client.DeleteRecord(zone.ID, recordset.ID, record.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("hyperone: fqdn=%s, zone ID=%s, recordset ID=%s, record ID=%s: %w", fqdn, zone.ID, recordset.ID, record.ID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("hyperone: fqdn=%s, failed to find record with given value", fqdn)
|
||||
}
|
||||
|
||||
// getHostedZone gets the hosted zone.
|
||||
func (d *DNSProvider) getHostedZone(fqdn string) (*internal.Zone, error) {
|
||||
authZone, err := dns01.FindZoneByFqdn(fqdn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return d.client.FindZone(authZone)
|
||||
}
|
||||
|
||||
func GetDefaultPassportLocation() (string, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get user home directory: %w", err)
|
||||
}
|
||||
|
||||
return filepath.Join(homeDir, ".h1", "passport.json"), nil
|
||||
}
|
46
providers/dns/hyperone/hyperone.toml
Normal file
46
providers/dns/hyperone/hyperone.toml
Normal file
|
@ -0,0 +1,46 @@
|
|||
Name = "HyperOne"
|
||||
Description = ''''''
|
||||
URL = "https://www.hyperone.com"
|
||||
Code = "hyperone"
|
||||
|
||||
Example = '''
|
||||
lego --dns hyperone --domains my.domain.com --email my@email.com run
|
||||
'''
|
||||
|
||||
Additional = '''
|
||||
## Description
|
||||
|
||||
Default configuration does not require any additional environment variables,
|
||||
just a passport file in `~/.h1/passport.json` location.
|
||||
|
||||
### Generating passport file using H1 CLI
|
||||
|
||||
To use this application you have to generate passport file for `sa`:
|
||||
|
||||
```
|
||||
h1 sa credential generate --name my-passport --sa <sa ID> --passport-output-file ~/.h1/passport.json
|
||||
```
|
||||
|
||||
### Required permissions
|
||||
|
||||
Depending of environment variables usage, the application requires different permissions:
|
||||
- `dns/zone/list` if `HYPERONE_ZONE_URI` is not specified
|
||||
- `dns/zone.recordset/list`
|
||||
- `dns/zone.recordset/create`
|
||||
- `dns/zone.recordset/delete`
|
||||
- `dns/zone.record/create`
|
||||
- `dns/zone.record/list`
|
||||
- `dns/zone.record/delete`
|
||||
'''
|
||||
|
||||
[Configuration]
|
||||
[Configuration.Additional]
|
||||
HYPERONE_PASSPORT_LOCATION = "Allows to pass custom passport file location (default ~/.h1/passport.json)"
|
||||
HYPERONE_API_URL = "Allows to pass custom API Endpoint to be used in the challenge (default https://api.hyperone.com/v2)"
|
||||
HYPERONE_LOCATION_ID = "Specifies location (region) to be used in API calls. (default pl-waw-1)"
|
||||
HYPERONE_TTL = "The TTL of the TXT record used for the DNS challenge"
|
||||
HYPERONE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
|
||||
HYPERONE_POLLING_INTERVAL = "Time between DNS propagation check"
|
||||
|
||||
[Links]
|
||||
API = "https://api.hyperone.com/v2/docs"
|
145
providers/dns/hyperone/hyperone_test.go
Normal file
145
providers/dns/hyperone/hyperone_test.go
Normal file
|
@ -0,0 +1,145 @@
|
|||
package hyperone
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v3/platform/tester"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const envDomain = envNamespace + "DOMAIN"
|
||||
|
||||
var envTest = tester.NewEnvTest(EnvPassportLocation, EnvAPIUrl, EnvLocationID).
|
||||
WithDomain(envDomain)
|
||||
|
||||
func TestNewDNSProvider(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
envVars map[string]string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "success",
|
||||
envVars: map[string]string{
|
||||
EnvPassportLocation: "./internal/fixtures/validPassport.json",
|
||||
EnvAPIUrl: "",
|
||||
EnvLocationID: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "invalid passport",
|
||||
envVars: map[string]string{
|
||||
EnvPassportLocation: "./internal/fixtures/invalidPassport.json",
|
||||
EnvAPIUrl: "",
|
||||
EnvLocationID: "",
|
||||
},
|
||||
expected: "hyperone: passport file validation failed: private key is missing",
|
||||
},
|
||||
{
|
||||
desc: "non existing passport",
|
||||
envVars: map[string]string{
|
||||
EnvPassportLocation: "./internal/fixtures/non-existing.json",
|
||||
EnvAPIUrl: "",
|
||||
EnvLocationID: "",
|
||||
},
|
||||
expected: "hyperone: failed to open passport file: open ./internal/fixtures/non-existing.json:",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
defer envTest.RestoreEnv()
|
||||
envTest.ClearEnv()
|
||||
|
||||
envTest.Apply(test.envVars)
|
||||
|
||||
p, err := NewDNSProvider()
|
||||
|
||||
if len(test.expected) == 0 {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, p)
|
||||
require.NotNil(t, p.config)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), test.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDNSProviderConfig(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
passportLocation string
|
||||
apiEndpoint string
|
||||
locationID string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "success",
|
||||
passportLocation: "./internal/fixtures/validPassport.json",
|
||||
apiEndpoint: "",
|
||||
locationID: "",
|
||||
},
|
||||
{
|
||||
desc: "invalid passport",
|
||||
passportLocation: "./internal/fixtures/invalidPassport.json",
|
||||
apiEndpoint: "",
|
||||
locationID: "",
|
||||
expected: "hyperone: passport file validation failed: private key is missing",
|
||||
},
|
||||
{
|
||||
desc: "non existing passport",
|
||||
passportLocation: "./internal/fixtures/non-existing.json",
|
||||
apiEndpoint: "",
|
||||
locationID: "",
|
||||
expected: "hyperone: failed to open passport file: open ./internal/fixtures/non-existing.json:",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
config := NewDefaultConfig()
|
||||
config.PassportLocation = test.passportLocation
|
||||
config.APIEndpoint = test.apiEndpoint
|
||||
config.LocationID = test.locationID
|
||||
|
||||
p, err := NewDNSProviderConfig(config)
|
||||
|
||||
if len(test.expected) == 0 {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, p)
|
||||
require.NotNil(t, p.config)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), test.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLivePresent(t *testing.T) {
|
||||
if !envTest.IsLiveTest() {
|
||||
t.Skip("skipping live test")
|
||||
}
|
||||
|
||||
envTest.RestoreEnv()
|
||||
provider, err := NewDNSProvider()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = provider.Present(envTest.GetDomain(), "", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLiveCleanUp(t *testing.T) {
|
||||
if !envTest.IsLiveTest() {
|
||||
t.Skip("skipping live test")
|
||||
}
|
||||
|
||||
envTest.RestoreEnv()
|
||||
provider, err := NewDNSProvider()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
325
providers/dns/hyperone/internal/client.go
Normal file
325
providers/dns/hyperone/internal/client.go
Normal file
|
@ -0,0 +1,325 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
const defaultBaseURL = "https://api.hyperone.com/v2"
|
||||
|
||||
const defaultLocationID = "pl-waw-1"
|
||||
|
||||
type signer interface {
|
||||
GetJWT() (string, error)
|
||||
}
|
||||
|
||||
// Client the HyperOne client.
|
||||
type Client struct {
|
||||
HTTPClient *http.Client
|
||||
|
||||
apiEndpoint string
|
||||
locationID string
|
||||
projectID string
|
||||
|
||||
passport *Passport
|
||||
signer signer
|
||||
}
|
||||
|
||||
// NewClient Creates a new HyperOne client.
|
||||
func NewClient(apiEndpoint, locationID string, passport *Passport) (*Client, error) {
|
||||
if passport == nil {
|
||||
return nil, errors.New("the passport is missing")
|
||||
}
|
||||
|
||||
projectID, err := passport.ExtractProjectID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
baseURL := defaultBaseURL
|
||||
if apiEndpoint != "" {
|
||||
baseURL = apiEndpoint
|
||||
}
|
||||
|
||||
tokenSigner := &TokenSigner{
|
||||
PrivateKey: passport.PrivateKey,
|
||||
KeyID: passport.CertificateID,
|
||||
Audience: baseURL,
|
||||
Issuer: passport.Issuer,
|
||||
Subject: passport.SubjectID,
|
||||
}
|
||||
|
||||
client := &Client{
|
||||
HTTPClient: &http.Client{Timeout: 5 * time.Second},
|
||||
apiEndpoint: baseURL,
|
||||
locationID: locationID,
|
||||
passport: passport,
|
||||
projectID: projectID,
|
||||
signer: tokenSigner,
|
||||
}
|
||||
|
||||
if client.locationID == "" {
|
||||
client.locationID = defaultLocationID
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// FindRecordset looks for recordset with given recordType and name and returns it.
|
||||
// In case if recordset is not found returns nil.
|
||||
// https://api.hyperone.com/v2/docs#operation/dns_project_zone_recordset_list
|
||||
func (c *Client) FindRecordset(zoneID, recordType, name string) (*Recordset, error) {
|
||||
// https://api.hyperone.com/v2/dns/{locationId}/project/{projectId}/zone/{zoneId}/recordset
|
||||
resourceURL := path.Join("dns", c.locationID, "project", c.projectID, "zone", zoneID, "recordset")
|
||||
|
||||
req, err := c.createRequest(http.MethodGet, resourceURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var recordSets []Recordset
|
||||
|
||||
err = c.do(req, &recordSets)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get recordsets from server: %w", err)
|
||||
}
|
||||
|
||||
for _, v := range recordSets {
|
||||
if v.RecordType == recordType && v.Name == name {
|
||||
return &v, nil
|
||||
}
|
||||
}
|
||||
|
||||
// when recordset is not present returns nil, but error is not thrown
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// CreateRecordset creates recordset and record with given value within one request.
|
||||
// https://api.hyperone.com/v2/docs#operation/dns_project_zone_recordset_create
|
||||
func (c *Client) CreateRecordset(zoneID, recordType, name, recordValue string, ttl int) (*Recordset, error) {
|
||||
recordsetInput := Recordset{
|
||||
RecordType: recordType,
|
||||
Name: name,
|
||||
TTL: ttl,
|
||||
Record: &Record{Content: recordValue},
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(recordsetInput)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal recordset: %w", err)
|
||||
}
|
||||
|
||||
// https://api.hyperone.com/v2/dns/{locationId}/project/{projectId}/zone/{zoneId}/recordset
|
||||
resourceURL := path.Join("dns", c.locationID, "project", c.projectID, "zone", zoneID, "recordset")
|
||||
|
||||
req, err := c.createRequest(http.MethodPost, resourceURL, bytes.NewBuffer(requestBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var recordsetResponse Recordset
|
||||
|
||||
err = c.do(req, &recordsetResponse)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create recordset: %w", err)
|
||||
}
|
||||
|
||||
return &recordsetResponse, nil
|
||||
}
|
||||
|
||||
// DeleteRecordset deletes a recordset.
|
||||
// https://api.hyperone.com/v2/docs#operation/dns_project_zone_recordset_delete
|
||||
func (c *Client) DeleteRecordset(zoneID string, recordsetID string) error {
|
||||
// https://api.hyperone.com/v2/dns/{locationId}/project/{projectId}/zone/{zoneId}/recordset/{recordsetId}
|
||||
resourceURL := path.Join("dns", c.locationID, "project", c.projectID, "zone", zoneID, "recordset", recordsetID)
|
||||
|
||||
req, err := c.createRequest(http.MethodDelete, resourceURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.do(req, nil)
|
||||
}
|
||||
|
||||
// GetRecords gets all records within specified recordset.
|
||||
// https://api.hyperone.com/v2/docs#operation/dns_project_zone_recordset_record_list
|
||||
func (c *Client) GetRecords(zoneID string, recordsetID string) ([]Record, error) {
|
||||
// https://api.hyperone.com/v2/dns/{locationId}/project/{projectId}/zone/{zoneId}/recordset/{recordsetId}/record
|
||||
resourceURL := path.Join("dns", c.locationID, "project", c.projectID, "zone", zoneID, "recordset", recordsetID, "record")
|
||||
|
||||
req, err := c.createRequest(http.MethodGet, resourceURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var records []Record
|
||||
|
||||
err = c.do(req, &records)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get records from server: %w", err)
|
||||
}
|
||||
|
||||
return records, err
|
||||
}
|
||||
|
||||
// CreateRecord creates a record.
|
||||
// https://api.hyperone.com/v2/docs#operation/dns_project_zone_recordset_record_create
|
||||
func (c *Client) CreateRecord(zoneID, recordsetID, recordContent string) (*Record, error) {
|
||||
// https://api.hyperone.com/v2/dns/{locationId}/project/{projectId}/zone/{zoneId}/recordset/{recordsetId}/record
|
||||
resourceURL := path.Join("dns", c.locationID, "project", c.projectID, "zone", zoneID, "recordset", recordsetID, "record")
|
||||
|
||||
requestBody, err := json.Marshal(Record{Content: recordContent})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal record: %w", err)
|
||||
}
|
||||
|
||||
req, err := c.createRequest(http.MethodPost, resourceURL, bytes.NewBuffer(requestBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var recordResponse Record
|
||||
|
||||
err = c.do(req, &recordResponse)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to set record: %w", err)
|
||||
}
|
||||
|
||||
return &recordResponse, nil
|
||||
}
|
||||
|
||||
// DeleteRecord deletes a record.
|
||||
// https://api.hyperone.com/v2/docs#operation/dns_project_zone_recordset_record_delete
|
||||
func (c *Client) DeleteRecord(zoneID, recordsetID, recordID string) error {
|
||||
// https://api.hyperone.com/v2/dns/{locationId}/project/{projectId}/zone/{zoneId}/recordset/{recordsetId}/record/{recordId}
|
||||
resourceURL := path.Join("dns", c.locationID, "project", c.projectID, "zone", zoneID, "recordset", recordsetID, "record", recordID)
|
||||
|
||||
req, err := c.createRequest(http.MethodDelete, resourceURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.do(req, nil)
|
||||
}
|
||||
|
||||
// FindZone looks for DNS Zone and returns nil if it does not exist.
|
||||
func (c *Client) FindZone(name string) (*Zone, error) {
|
||||
zones, err := c.GetZones()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, zone := range zones {
|
||||
if zone.DNSName == name {
|
||||
return &zone, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("failed to find zone for %s", name)
|
||||
}
|
||||
|
||||
// GetZones gets all user's zones.
|
||||
// https://api.hyperone.com/v2/docs#operation/dns_project_zone_list
|
||||
func (c *Client) GetZones() ([]Zone, error) {
|
||||
// https://api.hyperone.com/v2/dns/{locationId}/project/{projectId}/zone
|
||||
resourceURL := path.Join("dns", c.locationID, "project", c.projectID, "zone")
|
||||
|
||||
req, err := c.createRequest(http.MethodGet, resourceURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var zones []Zone
|
||||
|
||||
err = c.do(req, &zones)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch available zones: %w", err)
|
||||
}
|
||||
|
||||
return zones, nil
|
||||
}
|
||||
|
||||
func (c *Client) createRequest(method, uri string, body io.Reader) (*http.Request, error) {
|
||||
baseURL, err := url.Parse(c.apiEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
endpoint, err := baseURL.Parse(path.Join(baseURL.Path, uri))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, endpoint.String(), body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jwt, err := c.signer.GetJWT()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign the request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+jwt)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (c *Client) do(req *http.Request, v interface{}) error {
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
err = checkResponse(resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
raw, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read body: %w", err)
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(raw, v); err != nil {
|
||||
return fmt.Errorf("unmarshaling %T error: %w: %s", v, err, string(raw))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkResponse(resp *http.Response) error {
|
||||
if resp.StatusCode/100 == 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var msg string
|
||||
if resp.StatusCode == http.StatusForbidden {
|
||||
msg = "forbidden: check if service account you are trying to use has permissions required for managing DNS"
|
||||
} else {
|
||||
msg = fmt.Sprintf("%d: unknown error", resp.StatusCode)
|
||||
}
|
||||
|
||||
// add response body to error message if not empty
|
||||
responseBody, _ := ioutil.ReadAll(resp.Body)
|
||||
if len(responseBody) > 0 {
|
||||
msg = fmt.Sprintf("%s: %s", msg, string(responseBody))
|
||||
}
|
||||
|
||||
return errors.New(msg)
|
||||
}
|
219
providers/dns/hyperone/internal/client_test.go
Normal file
219
providers/dns/hyperone/internal/client_test.go
Normal file
|
@ -0,0 +1,219 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type signerMock struct{}
|
||||
|
||||
func (s signerMock) GetJWT() (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func TestClient_FindRecordset(t *testing.T) {
|
||||
client := setupTest(t, http.MethodGet, "/dns/loc123/project/proj123/zone/zone321/recordset", respFromFile("recordset.json"))
|
||||
|
||||
recordset, err := client.FindRecordset("zone321", "SOA", "example.com.")
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := &Recordset{
|
||||
ID: "123456789abcd",
|
||||
Name: "example.com.",
|
||||
RecordType: "SOA",
|
||||
TTL: 1800,
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, recordset)
|
||||
}
|
||||
|
||||
func TestClient_CreateRecordset(t *testing.T) {
|
||||
expectedReqBody := Recordset{
|
||||
RecordType: "TXT",
|
||||
Name: "test.example.com.",
|
||||
TTL: 3600,
|
||||
Record: &Record{Content: "value"},
|
||||
}
|
||||
|
||||
client := setupTest(t, http.MethodPost, "/dns/loc123/project/proj123/zone/zone123/recordset",
|
||||
hasReqBody(expectedReqBody), respFromFile("createRecordset.json"))
|
||||
|
||||
rs, err := client.CreateRecordset("zone123", "TXT", "test.example.com.", "value", 3600)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := &Recordset{RecordType: "TXT", Name: "test.example.com.", TTL: 3600, ID: "1234567890qwertyuiop"}
|
||||
assert.Equal(t, expected, rs)
|
||||
}
|
||||
|
||||
func TestClient_DeleteRecordset(t *testing.T) {
|
||||
client := setupTest(t, http.MethodDelete, "/dns/loc123/project/proj123/zone/zone321/recordset/rs322")
|
||||
|
||||
err := client.DeleteRecordset("zone321", "rs322")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestClient_GetRecords(t *testing.T) {
|
||||
client := setupTest(t, http.MethodGet, "/dns/loc123/project/proj123/zone/321/recordset/322/record", respFromFile("record.json"))
|
||||
|
||||
records, err := client.GetRecords("321", "322")
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := []Record{
|
||||
{
|
||||
ID: "135128352183572dd",
|
||||
Content: "pns.hyperone.com. hostmaster.hyperone.com. 1 15 180 1209600 1800",
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, records)
|
||||
}
|
||||
|
||||
func TestClient_CreateRecord(t *testing.T) {
|
||||
expectedReqBody := Record{
|
||||
Content: "value",
|
||||
}
|
||||
|
||||
client := setupTest(t, http.MethodPost, "/dns/loc123/project/proj123/zone/z123/recordset/rs325/record",
|
||||
hasReqBody(expectedReqBody), respFromFile("createRecord.json"))
|
||||
|
||||
rs, err := client.CreateRecord("z123", "rs325", "value")
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := &Record{ID: "123321qwerqwewqerq", Content: "value", Enabled: true}
|
||||
assert.Equal(t, expected, rs)
|
||||
}
|
||||
|
||||
func TestClient_DeleteRecord(t *testing.T) {
|
||||
client := setupTest(t, http.MethodDelete, "/dns/loc123/project/proj123/zone/321/recordset/322/record/323")
|
||||
|
||||
err := client.DeleteRecord("321", "322", "323")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestClient_FindZone(t *testing.T) {
|
||||
client := setupTest(t, http.MethodGet, "/dns/loc123/project/proj123/zone", respFromFile("zones.json"))
|
||||
|
||||
zone, err := client.FindZone("example.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := &Zone{
|
||||
ID: "zoneB",
|
||||
Name: "example.com",
|
||||
DNSName: "example.com",
|
||||
FQDN: "example.com.",
|
||||
URI: "",
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, zone)
|
||||
}
|
||||
|
||||
func TestClient_GetZones(t *testing.T) {
|
||||
client := setupTest(t, http.MethodGet, "/dns/loc123/project/proj123/zone", respFromFile("zones.json"))
|
||||
|
||||
zones, err := client.GetZones()
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := []Zone{
|
||||
{
|
||||
ID: "zoneA",
|
||||
Name: "example.org",
|
||||
DNSName: "example.org",
|
||||
FQDN: "example.org.",
|
||||
URI: "",
|
||||
},
|
||||
{
|
||||
ID: "zoneB",
|
||||
Name: "example.com",
|
||||
DNSName: "example.com",
|
||||
FQDN: "example.com.",
|
||||
URI: "",
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, zones)
|
||||
}
|
||||
|
||||
func setupTest(t *testing.T, method, path string, handlers ...assertHandler) *Client {
|
||||
mux := http.NewServeMux()
|
||||
server := httptest.NewServer(mux)
|
||||
|
||||
mux.Handle(path, http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != method {
|
||||
http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
if len(handlers) != 0 {
|
||||
for _, handler := range handlers {
|
||||
code, err := handler(rw, req)
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), code)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
passport := &Passport{
|
||||
SubjectID: "/iam/project/proj123/sa/xxxxxxx",
|
||||
}
|
||||
|
||||
client, err := NewClient(server.URL, "loc123", passport)
|
||||
require.NoError(t, err)
|
||||
|
||||
client.signer = signerMock{}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
type assertHandler func(http.ResponseWriter, *http.Request) (int, error)
|
||||
|
||||
func hasReqBody(v interface{}) assertHandler {
|
||||
return func(rw http.ResponseWriter, req *http.Request) (int, error) {
|
||||
reqBody, err := ioutil.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
marshal, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
if !bytes.Equal(marshal, reqBody) {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid request body, got: %s, expect: %s", string(reqBody), string(marshal))
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
}
|
||||
|
||||
func respFromFile(fixtureName string) assertHandler {
|
||||
return func(rw http.ResponseWriter, req *http.Request) (int, error) {
|
||||
file, err := os.Open(filepath.Join(".", "fixtures", fixtureName))
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
_, err = io.Copy(rw, file)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"id": "123321qwerqwewqerq",
|
||||
"content": "value",
|
||||
"enabled": true
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"id": "1234567890qwertyuiop",
|
||||
"name": "test.example.com.",
|
||||
"type": "TXT",
|
||||
"ttl": 3600
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"subject_id": "/iam/project/projectId/sa/serviceAccountId",
|
||||
"certificate_id": "certificateID",
|
||||
"issuer": "https://api.hyperone.com/v2/iam/project/projectId/sa/serviceAccountId"
|
||||
}
|
7
providers/dns/hyperone/internal/fixtures/record.json
Normal file
7
providers/dns/hyperone/internal/fixtures/record.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
[
|
||||
{
|
||||
"id": "135128352183572dd",
|
||||
"content": "pns.hyperone.com. hostmaster.hyperone.com. 1 15 180 1209600 1800",
|
||||
"enabled": true
|
||||
}
|
||||
]
|
20
providers/dns/hyperone/internal/fixtures/recordset.json
Normal file
20
providers/dns/hyperone/internal/fixtures/recordset.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
[
|
||||
{
|
||||
"id": "123456789abcd",
|
||||
"name": "example.com.",
|
||||
"type": "SOA",
|
||||
"ttl": 1800
|
||||
},
|
||||
{
|
||||
"id": "123456789abcde",
|
||||
"name": "example.com.",
|
||||
"type": "NS",
|
||||
"ttl": 3600
|
||||
},
|
||||
{
|
||||
"id": "123456789abcdf",
|
||||
"name": "example.com.",
|
||||
"type": "CNAME",
|
||||
"ttl": 3600
|
||||
}
|
||||
]
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"subject_id": "/iam/project/projectId/sa/serviceAccountId",
|
||||
"certificate_id": "certificateID",
|
||||
"issuer": "https://api.hyperone.com/v2/iam/project/projectId/sa/serviceAccountId",
|
||||
"private_key": "-----BEGIN RSA PRIVATE KEY-----\nlrMAsSjjkKiRxGdgR8p5kZJj0AFgdWYa3OT2snIXnN5+/p7j13PSkseUcrAFyokc\nV9pgeDfitAhb9lpdjxjjuxRcuQjBfmNVLPF9MFyNOvhrprGNukUh/12oSKO9dFEt\ns39F/2h6Ld5IQrGt3gZaBB1aGO+tw3ill1VBy2zGPIDeuSz6DS3GG/oQ2gLSSMP4\nOVfQ32Oajo496iHRkdIh/7Hho7BNzMYr1GxrYTcE9/Znr6xgeSdNT37CCeCH8cmP\naEAUgSMTeIMVSpILwkKeNvBURic1EWaqXRgPRIWK0vNyOCs/+jNoFISnV4pu1ROF\n92vayHDNSVw9wHcdSQ75XSE4Msawqv5U1iI7e2lD64uo1qhmJdrPcXDJQCiDbh+F\nhQhF+wAoLRvMNwwhg+LttL8vXqMDQl3olsWSvWPs6b/MZpB0qwd1bklzA6P+PeAU\nsfOvTqi9edIOfKqvXqTXEhBP8qC7ZtOKLGnryZb7W04SSVrNtuJUFRcLiqu+w/F/\nMSxGSGalYpzIZ1B5HLQqISgWMXdbt39uMeeooeZjkuI3VIllFjtybecjPR9ZYQPt\nFFEP1XqNXjLFmGh84TXtvGLWretWM1OZmN8UKKUeATqrr7zuh5AYGAIbXd8BvweL\nPigl9ei0hTculPqohvkoc5x1srPBvzHrirGlxOYjW3fc4kDgZpy+6ik5k5g7JWQD\nlbXCRz3HGazgUPeiwUr06a52vhgT7QuNIUZqdHb4IfCYs2pQTLHzQjAqvVk1mm2D\nkh4myIcTtf69BFcu/Wuptm3NaKd1nwk1squR6psvcTXOWII81pstnxNYkrokx4r2\n7YVllNruOD+cMDNZbIG2CwT6V9ukIS8tl9EJp8eyb0a1uAEc22BNOjYHPF50beWF\nukf3uc0SA+G3zhmXCM5sMf5OxVjKr5jgcir7kySY5KbmG71omYhczgr4H0qgxYo9\nZyj2wMKrTHLfFOpd4OOEun9Gi3srqlKZep7Hj7gNyUwZu1qiBvElmBVmp0HJxT0N\nmktuaVbaFgBsTS0/us1EqWvCA4REh1Ut/NoA9oG3JFt0lGDstTw1j+orDmIHOmSu\n7FKYzr0uCz14AkLMSOixdPD1F0YyED1NMVnRVXw77HiAFGmb0CDi2KEg70pEKpn3\nksa8oe0MQi6oEwlMsAxVTXOB1wblTBuSBeaECzTzWE+/DHF+QQfQi8kAjjSdmmMJ\nyN+shdBWHYRGYnxRkTatONhcDBIY7sZV7wolYHz/rf7dpYUZf37vdQnYV8FpO1um\nYa0GslyRJ5GqMBfDS1cQKne+FvVHxEE2YqEGBcOYhx/JI2soE8aA8W4XffN+DoEy\nZkinJ/+BOwJ/zUI9GZtwB4JXqbNEE+j7r7/fJO9KxfPp4MPK4YWu0H0EUWONpVwe\nTWtbRhQUCOe4PVSC/Vv1pstvMD/D+E/0L4GQNHxr+xyFxuvILty5lvFTxoAVYpqD\nu8gNhk3NWefTrlSkhY4N+tPP6o7E4t3y40nOA/d9qaqiid+lYcIDB0cJTpZvgeeQ\nijohxY3PHruU4vVZa37ITQnco9az6lsy18vbU0bOyK2fEZ2R9XVO8fH11jiV8oGH\n-----END RSA PRIVATE KEY-----\n",
|
||||
"public_key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxzYuc22QSst/dS7geYYK\n5l5kLxU0tayNdixkEQ17ix+CUcUbKIsnyftZxaCYT46rQtXgCaYRdJcbB3hmyrOa\nvkhTpX79xJZnQmfuamMbZBqitvscxW9zRR9tBUL6vdi/0rpoUwPMEh8+Bw7CgYR0\nFK0DhWYBNDfe9HKcyZEv3max8Cdq18htxjEsdYO0iwzhtKRXomBWTdhD5ykd/fAC\nVTr4+KEY+IeLvubHVmLUhbE5NgWXxrRpGasDqzKhCTmsa2Ysf712rl57SlH0Wz/M\nr3F7aM9YpErzeYLrl0GhQr9BVJxOvXcVd4kmY+XkiCcrkyS1cnghnllh+LCwQu1s\nYwIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
}
|
16
providers/dns/hyperone/internal/fixtures/zones.json
Normal file
16
providers/dns/hyperone/internal/fixtures/zones.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
[
|
||||
{
|
||||
"id": "zoneA",
|
||||
"name": "example.org",
|
||||
"dnsName": "example.org",
|
||||
"fqdn": "example.org.",
|
||||
"uri": ""
|
||||
},
|
||||
{
|
||||
"id": "zoneB",
|
||||
"name": "example.com",
|
||||
"dnsName": "example.com",
|
||||
"fqdn": "example.com.",
|
||||
"uri": ""
|
||||
}
|
||||
]
|
23
providers/dns/hyperone/internal/models.go
Normal file
23
providers/dns/hyperone/internal/models.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package internal
|
||||
|
||||
type Recordset struct {
|
||||
RecordType string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
TTL int `json:"ttl,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Record *Record `json:"record,omitempty"`
|
||||
}
|
||||
|
||||
type Record struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Content string `json:"content"`
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
}
|
||||
|
||||
type Zone struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
DNSName string `json:"dnsName"`
|
||||
FQDN string `json:"fqdn"`
|
||||
URI string `json:"uri"`
|
||||
}
|
70
providers/dns/hyperone/internal/passport.go
Normal file
70
providers/dns/hyperone/internal/passport.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type Passport struct {
|
||||
SubjectID string `json:"subject_id"`
|
||||
CertificateID string `json:"certificate_id"`
|
||||
Issuer string `json:"issuer"`
|
||||
PrivateKey string `json:"private_key"`
|
||||
PublicKey string `json:"public_key"`
|
||||
}
|
||||
|
||||
func LoadPassportFile(location string) (*Passport, error) {
|
||||
file, err := os.Open(location)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open passport file: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
var passport Passport
|
||||
err = json.NewDecoder(file).Decode(&passport)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse passport file: %w", err)
|
||||
}
|
||||
|
||||
err = passport.validate()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("passport file validation failed: %w", err)
|
||||
}
|
||||
|
||||
return &passport, nil
|
||||
}
|
||||
|
||||
func (passport *Passport) validate() error {
|
||||
if passport.Issuer == "" {
|
||||
return errors.New("issuer is empty")
|
||||
}
|
||||
|
||||
if passport.CertificateID == "" {
|
||||
return errors.New("certificate ID is empty")
|
||||
}
|
||||
|
||||
if passport.PrivateKey == "" {
|
||||
return errors.New("private key is missing")
|
||||
}
|
||||
|
||||
if passport.SubjectID == "" {
|
||||
return errors.New("subject is empty")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (passport *Passport) ExtractProjectID() (string, error) {
|
||||
re := regexp.MustCompile("iam/project/([a-zA-Z0-9]+)")
|
||||
|
||||
parts := re.FindStringSubmatch(passport.SubjectID)
|
||||
if len(parts) != 2 {
|
||||
return "", fmt.Errorf("failed to extract project ID from subject ID: %s", passport.SubjectID)
|
||||
}
|
||||
|
||||
return parts[1], nil
|
||||
}
|
83
providers/dns/hyperone/internal/passport_test.go
Normal file
83
providers/dns/hyperone/internal/passport_test.go
Normal file
|
@ -0,0 +1,83 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLoadPassportFile(t *testing.T) {
|
||||
passport, err := LoadPassportFile("fixtures/validPassport.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := &Passport{
|
||||
SubjectID: "/iam/project/projectId/sa/serviceAccountId",
|
||||
CertificateID: "certificateID",
|
||||
Issuer: "https://api.hyperone.com/v2/iam/project/projectId/sa/serviceAccountId",
|
||||
PrivateKey: `-----BEGIN RSA PRIVATE KEY-----
|
||||
lrMAsSjjkKiRxGdgR8p5kZJj0AFgdWYa3OT2snIXnN5+/p7j13PSkseUcrAFyokc
|
||||
V9pgeDfitAhb9lpdjxjjuxRcuQjBfmNVLPF9MFyNOvhrprGNukUh/12oSKO9dFEt
|
||||
s39F/2h6Ld5IQrGt3gZaBB1aGO+tw3ill1VBy2zGPIDeuSz6DS3GG/oQ2gLSSMP4
|
||||
OVfQ32Oajo496iHRkdIh/7Hho7BNzMYr1GxrYTcE9/Znr6xgeSdNT37CCeCH8cmP
|
||||
aEAUgSMTeIMVSpILwkKeNvBURic1EWaqXRgPRIWK0vNyOCs/+jNoFISnV4pu1ROF
|
||||
92vayHDNSVw9wHcdSQ75XSE4Msawqv5U1iI7e2lD64uo1qhmJdrPcXDJQCiDbh+F
|
||||
hQhF+wAoLRvMNwwhg+LttL8vXqMDQl3olsWSvWPs6b/MZpB0qwd1bklzA6P+PeAU
|
||||
sfOvTqi9edIOfKqvXqTXEhBP8qC7ZtOKLGnryZb7W04SSVrNtuJUFRcLiqu+w/F/
|
||||
MSxGSGalYpzIZ1B5HLQqISgWMXdbt39uMeeooeZjkuI3VIllFjtybecjPR9ZYQPt
|
||||
FFEP1XqNXjLFmGh84TXtvGLWretWM1OZmN8UKKUeATqrr7zuh5AYGAIbXd8BvweL
|
||||
Pigl9ei0hTculPqohvkoc5x1srPBvzHrirGlxOYjW3fc4kDgZpy+6ik5k5g7JWQD
|
||||
lbXCRz3HGazgUPeiwUr06a52vhgT7QuNIUZqdHb4IfCYs2pQTLHzQjAqvVk1mm2D
|
||||
kh4myIcTtf69BFcu/Wuptm3NaKd1nwk1squR6psvcTXOWII81pstnxNYkrokx4r2
|
||||
7YVllNruOD+cMDNZbIG2CwT6V9ukIS8tl9EJp8eyb0a1uAEc22BNOjYHPF50beWF
|
||||
ukf3uc0SA+G3zhmXCM5sMf5OxVjKr5jgcir7kySY5KbmG71omYhczgr4H0qgxYo9
|
||||
Zyj2wMKrTHLfFOpd4OOEun9Gi3srqlKZep7Hj7gNyUwZu1qiBvElmBVmp0HJxT0N
|
||||
mktuaVbaFgBsTS0/us1EqWvCA4REh1Ut/NoA9oG3JFt0lGDstTw1j+orDmIHOmSu
|
||||
7FKYzr0uCz14AkLMSOixdPD1F0YyED1NMVnRVXw77HiAFGmb0CDi2KEg70pEKpn3
|
||||
ksa8oe0MQi6oEwlMsAxVTXOB1wblTBuSBeaECzTzWE+/DHF+QQfQi8kAjjSdmmMJ
|
||||
yN+shdBWHYRGYnxRkTatONhcDBIY7sZV7wolYHz/rf7dpYUZf37vdQnYV8FpO1um
|
||||
Ya0GslyRJ5GqMBfDS1cQKne+FvVHxEE2YqEGBcOYhx/JI2soE8aA8W4XffN+DoEy
|
||||
ZkinJ/+BOwJ/zUI9GZtwB4JXqbNEE+j7r7/fJO9KxfPp4MPK4YWu0H0EUWONpVwe
|
||||
TWtbRhQUCOe4PVSC/Vv1pstvMD/D+E/0L4GQNHxr+xyFxuvILty5lvFTxoAVYpqD
|
||||
u8gNhk3NWefTrlSkhY4N+tPP6o7E4t3y40nOA/d9qaqiid+lYcIDB0cJTpZvgeeQ
|
||||
ijohxY3PHruU4vVZa37ITQnco9az6lsy18vbU0bOyK2fEZ2R9XVO8fH11jiV8oGH
|
||||
-----END RSA PRIVATE KEY-----
|
||||
`,
|
||||
PublicKey: `-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxzYuc22QSst/dS7geYYK
|
||||
5l5kLxU0tayNdixkEQ17ix+CUcUbKIsnyftZxaCYT46rQtXgCaYRdJcbB3hmyrOa
|
||||
vkhTpX79xJZnQmfuamMbZBqitvscxW9zRR9tBUL6vdi/0rpoUwPMEh8+Bw7CgYR0
|
||||
FK0DhWYBNDfe9HKcyZEv3max8Cdq18htxjEsdYO0iwzhtKRXomBWTdhD5ykd/fAC
|
||||
VTr4+KEY+IeLvubHVmLUhbE5NgWXxrRpGasDqzKhCTmsa2Ysf712rl57SlH0Wz/M
|
||||
r3F7aM9YpErzeYLrl0GhQr9BVJxOvXcVd4kmY+XkiCcrkyS1cnghnllh+LCwQu1s
|
||||
YwIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
`,
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, passport)
|
||||
}
|
||||
|
||||
func TestLoadPassportFile_invalid(t *testing.T) {
|
||||
passport, err := LoadPassportFile("fixtures/invalidPassport.json")
|
||||
require.EqualError(t, err, "passport file validation failed: private key is missing")
|
||||
|
||||
assert.Nil(t, passport)
|
||||
}
|
||||
|
||||
func TestExtractProjectID(t *testing.T) {
|
||||
passport := Passport{SubjectID: "/iam/project/ddd/sa/5ef759c0ab0acab07xxxxxxx"}
|
||||
extractedID, err := passport.ExtractProjectID()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "ddd", extractedID)
|
||||
}
|
||||
|
||||
func TestExtractProjectID_invalid(t *testing.T) {
|
||||
passport := Passport{SubjectID: "ddddddd"}
|
||||
|
||||
extractedID, err := passport.ExtractProjectID()
|
||||
require.EqualError(t, err, "failed to extract project ID from subject ID: ddddddd")
|
||||
|
||||
assert.Empty(t, extractedID)
|
||||
}
|
85
providers/dns/hyperone/internal/token.go
Normal file
85
providers/dns/hyperone/internal/token.go
Normal file
|
@ -0,0 +1,85 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
"gopkg.in/square/go-jose.v2/jwt"
|
||||
)
|
||||
|
||||
type TokenSigner struct {
|
||||
PrivateKey string
|
||||
KeyID string
|
||||
Audience string
|
||||
Issuer string
|
||||
Subject string
|
||||
}
|
||||
|
||||
func (input *TokenSigner) GetJWT() (string, error) {
|
||||
signer, err := getRSASigner(input.PrivateKey, input.KeyID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
issuedAt := time.Now()
|
||||
expiresAt := issuedAt.Add(5 * time.Minute)
|
||||
|
||||
payload := Payload{IssuedAt: issuedAt.Unix(), Expiry: expiresAt.Unix(), Audience: input.Audience, Issuer: input.Issuer, Subject: input.Subject}
|
||||
token, err := payload.buildToken(&signer)
|
||||
|
||||
return token, err
|
||||
}
|
||||
|
||||
func getRSASigner(privateKey, keyID string) (jose.Signer, error) {
|
||||
parsedKey, err := parseRSAKey(privateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key := jose.SigningKey{Algorithm: jose.RS256, Key: parsedKey}
|
||||
|
||||
signerOpts := jose.SignerOptions{}
|
||||
signerOpts.WithType("JWT")
|
||||
signerOpts.WithHeader("kid", keyID)
|
||||
|
||||
rsaSigner, err := jose.NewSigner(key, &signerOpts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create JWS RSA256 signer: %w", err)
|
||||
}
|
||||
|
||||
return rsaSigner, nil
|
||||
}
|
||||
|
||||
type Payload struct {
|
||||
IssuedAt int64 `json:"iat"`
|
||||
Expiry int64 `json:"exp"`
|
||||
Audience string `json:"aud"`
|
||||
Issuer string `json:"iss"`
|
||||
Subject string `json:"sub"`
|
||||
}
|
||||
|
||||
func (payload *Payload) buildToken(signer *jose.Signer) (string, error) {
|
||||
builder := jwt.Signed(*signer).Claims(payload)
|
||||
|
||||
token, err := builder.CompactSerialize()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to build JWT: %w", err)
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func parseRSAKey(pemString string) (*rsa.PrivateKey, error) {
|
||||
block, _ := pem.Decode([]byte(pemString))
|
||||
|
||||
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse private key: %w", err)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
65
providers/dns/hyperone/internal/token_test.go
Normal file
65
providers/dns/hyperone/internal/token_test.go
Normal file
|
@ -0,0 +1,65 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const privateKey = `-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICWgIBAAKBgGFfgMY+DuO8l0RYrMLhcl6U/NigNIiOVhoo/xnYyoQALpWxBaBR
|
||||
+iVJiBUYunQjKA33yAiY0AasCfSn1JB6asayQvGGn73xztLjkeCVLT+9e4nJ0A/o
|
||||
dK8SOKBg9FFe70KJrWjJd626el0aVDJjtCE+QxJExA0UZbQp+XIyveQXAgMBAAEC
|
||||
gYBHcL1XNWLRPaWx9GlUVfoGYMMd4HSKl/ueF+QKP59dt5B2LTnWhS7FOqzH5auu
|
||||
17hkfx3ZCNzfeEuZn6T6F4bMtsQ6A5iT/DeRlG8tOPiCVZ/L0j6IFM78iIUT8XyA
|
||||
miwnSy1xGSBA67yUmsLxFg2DtGCjamAkY0C5pccadaB7oQJBAKsIPpMXMni+Oo1I
|
||||
kVxRyoIZgDxsMJiihG2YLVqo8rPtdErl+Lyg3ziVyg9KR6lFMaTBkYBTLoCPof3E
|
||||
AB/jyucCQQCRv1cVnYNx+bfnXsBlcsCFDV2HkEuLTpxj7hauD4P3GcyLidSsUkn1
|
||||
PiPunZqKpsQaIoxc/BzTOCcP19ifgqdRAkBJ8Cp9FE4xfKt7YJ/WtVVCoRubA3qO
|
||||
wdNWPa99vgQOXN0lc/3wLevSXo8XxRjtyIgJndT1EQDNe0qglhcnsiaJAkBziAcR
|
||||
/VAq0tZys2szf6kYTyXqxfj8Lo5NsHeN9oKXJ346xkEtb/VsT5vQFGJishsU1HoL
|
||||
Y1W+IO7l4iW3G6xhAkACNwtqxSRRbVsNCUMENpKmYhsyN8QXJ8V+o2A9s+pl21Kz
|
||||
HIIm179mUYCgO6iAHmkqxlFHFwprUBKdPrmP8qF9
|
||||
-----END RSA PRIVATE KEY-----`
|
||||
|
||||
type Header struct {
|
||||
Algorithm string `json:"alg"`
|
||||
Type string `json:"typ"`
|
||||
KeyID string `json:"kid"`
|
||||
}
|
||||
|
||||
func TestPayload_buildToken(t *testing.T) {
|
||||
signer, err := getRSASigner(privateKey, "sampleKeyId")
|
||||
require.NoError(t, err)
|
||||
|
||||
payload := Payload{IssuedAt: 1234, Expiry: 4321, Audience: "api.url", Issuer: "issuer", Subject: "subject"}
|
||||
|
||||
token, err := payload.buildToken(&signer)
|
||||
require.NoError(t, err)
|
||||
|
||||
segments := strings.Split(token, ".")
|
||||
require.Len(t, segments, 3)
|
||||
|
||||
headerString, err := base64.RawStdEncoding.DecodeString(segments[0])
|
||||
require.NoError(t, err)
|
||||
|
||||
var headerStruct Header
|
||||
err = json.Unmarshal(headerString, &headerStruct)
|
||||
require.NoError(t, err)
|
||||
|
||||
payloadString, err := base64.RawStdEncoding.DecodeString(segments[1])
|
||||
require.NoError(t, err)
|
||||
|
||||
var payloadStruct Payload
|
||||
err = json.Unmarshal(payloadString, &payloadStruct)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedHeader := Header{Algorithm: "RS256", Type: "JWT", KeyID: "sampleKeyId"}
|
||||
|
||||
assert.Equal(t, expectedHeader, headerStruct)
|
||||
assert.Equal(t, payload, payloadStruct)
|
||||
}
|
Loading…
Reference in a new issue