Skip to content

Commit 39b64d0

Browse files
feat: added arvancloud provider
1 parent 20ac110 commit 39b64d0

File tree

6 files changed

+262
-0
lines changed

6 files changed

+262
-0
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ This readme and the [docs/](docs/) directory are **versioned** to match the prog
5050
- Updates periodically A records for different DNS providers:
5151
- Aliyun
5252
- AllInkl
53+
- ArvanCloud
5354
- Changeip
5455
- Cloudflare
5556
- DD24
@@ -216,6 +217,7 @@ Check the documentation for your DNS provider:
216217

217218
- [Aliyun](docs/aliyun.md)
218219
- [Allinkl](docs/allinkl.md)
220+
- [ArvanCloud](docs/arvancloud.md)
219221
- [ChangeIP](docs/changeip.md)
220222
- [Cloudflare](docs/cloudflare.md)
221223
- [Custom](docs/custom.md)

docs/arvancloud.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Arvancloud.ir
2+
3+
## Configuration
4+
5+
### Example
6+
7+
```json
8+
{
9+
"settings": [
10+
{
11+
"provider": "arvancloud",
12+
"domain": "sub.domain.com",
13+
"token": "apikey ..."
14+
}
15+
]
16+
}
17+
```
18+
19+
### Compulsory parameters
20+
21+
- `"domain"` is the domain to update. It cannot be `example.com` (root domain) and should be like `sub.example.com` (subdomain of `example.com`).
22+
- `"token"` like "apikey ...".
23+
24+
## Domain setup
25+
26+
# Create a policy for managing DNS in [Policies](https://panel.arvancloud.ir/profile/iam/policies)
27+
# Create a token in [ArvanCloud profile](https://panel.arvancloud.ir/profile/iam/machine-users)
28+
# Give access of the policy to the token

internal/provider/constants/providers.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import "github.com/qdm12/ddns-updater/internal/models"
66
const (
77
Aliyun models.Provider = "aliyun"
88
AllInkl models.Provider = "allinkl"
9+
Arvancloud models.Provider = "arvancloud"
910
Changeip models.Provider = "changeip"
1011
Cloudflare models.Provider = "cloudflare"
1112
Custom models.Provider = "custom"

internal/provider/headers/headers.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ func SetAuthSSOKey(request *http.Request, key, secret string) {
2222
request.Header.Set("Authorization", "sso-key "+key+":"+secret)
2323
}
2424

25+
func SetAuthorization(request *http.Request, token string) {
26+
request.Header.Set("Authorization", token)
27+
}
28+
2529
func SetOauth(request *http.Request, value string) {
2630
request.Header.Set("Oauth", value)
2731
}

internal/provider/provider.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/qdm12/ddns-updater/internal/provider/constants"
1313
"github.com/qdm12/ddns-updater/internal/provider/providers/aliyun"
1414
"github.com/qdm12/ddns-updater/internal/provider/providers/allinkl"
15+
"github.com/qdm12/ddns-updater/internal/provider/providers/arvancloud"
1516
"github.com/qdm12/ddns-updater/internal/provider/providers/changeip"
1617
"github.com/qdm12/ddns-updater/internal/provider/providers/cloudflare"
1718
"github.com/qdm12/ddns-updater/internal/provider/providers/custom"
@@ -88,6 +89,8 @@ func New(providerName models.Provider, data json.RawMessage, domain, owner strin
8889
return aliyun.New(data, domain, owner, ipVersion, ipv6Suffix)
8990
case constants.AllInkl:
9091
return allinkl.New(data, domain, owner, ipVersion, ipv6Suffix)
92+
case constants.Arvancloud:
93+
return arvancloud.New(data, domain, owner, ipVersion, ipv6Suffix)
9194
case constants.Changeip:
9295
return changeip.New(data, domain, owner, ipVersion, ipv6Suffix)
9396
case constants.Cloudflare:
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
package arvancloud
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"net/http"
9+
"net/netip"
10+
"net/url"
11+
"strings"
12+
13+
"github.com/qdm12/ddns-updater/internal/models"
14+
"github.com/qdm12/ddns-updater/internal/provider/constants"
15+
"github.com/qdm12/ddns-updater/internal/provider/errors"
16+
"github.com/qdm12/ddns-updater/internal/provider/headers"
17+
"github.com/qdm12/ddns-updater/internal/provider/utils"
18+
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
19+
)
20+
21+
type Provider struct {
22+
domain string
23+
owner string
24+
token string
25+
ipVersion ipversion.IPVersion
26+
ipv6Suffix netip.Prefix
27+
}
28+
29+
func New(data json.RawMessage, domain, owner string,
30+
ipVersion ipversion.IPVersion, ipv6Suffix netip.Prefix) (
31+
provider *Provider, err error,
32+
) {
33+
var providerSpecificSettings struct {
34+
Token string `json:"token"`
35+
}
36+
err = json.Unmarshal(data, &providerSpecificSettings)
37+
if err != nil {
38+
return nil, fmt.Errorf("json decoding provider specific settings: %w", err)
39+
}
40+
41+
err = validateSettings(domain, owner, providerSpecificSettings.Token)
42+
if err != nil {
43+
return nil, fmt.Errorf("validating provider specific settings: %w", err)
44+
}
45+
46+
return &Provider{
47+
domain: domain,
48+
owner: owner,
49+
token: providerSpecificSettings.Token,
50+
}, nil
51+
}
52+
53+
func validateSettings(domain, owner, token string) (err error) {
54+
err = utils.CheckDomain(domain)
55+
if err != nil {
56+
return fmt.Errorf("%w: %w", errors.ErrDomainNotValid, err)
57+
}
58+
59+
switch {
60+
case owner == "*":
61+
return fmt.Errorf("%w", errors.ErrOwnerWildcard)
62+
case owner == "":
63+
return fmt.Errorf("%w", errors.ErrDomainNotValid)
64+
case token == "":
65+
return fmt.Errorf("%w ", errors.ErrKeyNotValid)
66+
case !strings.HasPrefix(token, "apikey "):
67+
return fmt.Errorf("%w: token should be like `apikey <your-api-key>`", errors.ErrKeyNotValid)
68+
}
69+
return nil
70+
}
71+
72+
func (p *Provider) String() string {
73+
return utils.ToString(p.domain, p.owner, constants.Arvancloud, p.ipVersion)
74+
}
75+
76+
func (p *Provider) Domain() string {
77+
return p.domain
78+
}
79+
80+
func (p *Provider) Owner() string {
81+
return p.owner
82+
}
83+
84+
func (p *Provider) IPVersion() ipversion.IPVersion {
85+
return p.ipVersion
86+
}
87+
88+
func (p *Provider) IPv6Suffix() netip.Prefix {
89+
return p.ipv6Suffix
90+
}
91+
92+
func (p *Provider) Proxied() bool {
93+
return false
94+
}
95+
96+
func (p *Provider) BuildDomainName() string {
97+
return utils.BuildDomainName(p.owner, p.domain)
98+
}
99+
100+
func (p *Provider) HTML() models.HTMLRow {
101+
return models.HTMLRow{
102+
Domain: fmt.Sprintf("<a href=\"http://%s\">%s</a>", p.BuildDomainName(), p.BuildDomainName()),
103+
Owner: p.Owner(),
104+
Provider: "<a href=\"https://arvancloud.ir/\">ArvanCloud</a>",
105+
IPVersion: p.ipVersion.String(),
106+
}
107+
}
108+
109+
// https://www.arvancloud.ir/api/cdn/4.0#tag/DNS-Management/operation/dns-records.show
110+
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
111+
domainID, err := p.getDomainID(ctx, client)
112+
if err != nil {
113+
return netip.Addr{}, err
114+
}
115+
116+
u := url.URL{
117+
Scheme: "https",
118+
Host: "napi.arvancloud.ir",
119+
Path: fmt.Sprintf("/cdn/4.0/domains/%s/dns-records/%s", p.domain, domainID),
120+
}
121+
122+
payload, err := json.Marshal(struct {
123+
Name string `json:"name"`
124+
Type string `json:"type"`
125+
Value []struct {
126+
IP string `json:"ip"`
127+
} `json:"value"`
128+
}{
129+
Name: p.owner,
130+
Type: "a",
131+
Value: []struct {
132+
IP string `json:"ip"`
133+
}{
134+
{
135+
IP: ip.String(),
136+
},
137+
},
138+
})
139+
140+
if err != nil {
141+
return netip.Addr{}, err
142+
}
143+
144+
request, err := http.NewRequestWithContext(ctx, http.MethodPut, u.String(), bytes.NewReader(payload))
145+
if err != nil {
146+
return netip.Addr{}, fmt.Errorf("creating http request: %w", err)
147+
}
148+
headers.SetUserAgent(request)
149+
headers.SetContentType(request, "application/json")
150+
headers.SetAccept(request, "application/json")
151+
headers.SetAuthorization(request, p.token)
152+
153+
response, err := client.Do(request)
154+
if err != nil {
155+
return netip.Addr{}, err
156+
}
157+
defer response.Body.Close()
158+
159+
s, err := utils.ReadAndCleanBody(response.Body)
160+
if err != nil {
161+
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
162+
}
163+
164+
if response.StatusCode != http.StatusOK {
165+
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
166+
errors.ErrHTTPStatusNotValid, response.StatusCode, utils.ToSingleLine(s))
167+
}
168+
169+
return ip, nil
170+
}
171+
172+
func (p *Provider) getDomainID(ctx context.Context, client *http.Client) (string, error) {
173+
u := url.URL{
174+
Scheme: "https",
175+
Host: "napi.arvancloud.ir",
176+
Path: fmt.Sprintf("/cdn/4.0/domains/%s/dns-records", p.domain),
177+
}
178+
179+
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
180+
if err != nil {
181+
return "", fmt.Errorf("creating http request: %w", err)
182+
}
183+
headers.SetUserAgent(request)
184+
headers.SetContentType(request, "application/json")
185+
headers.SetAccept(request, "application/json")
186+
headers.SetAuthorization(request, p.token)
187+
188+
response, err := client.Do(request)
189+
190+
if err != nil {
191+
return "", err
192+
}
193+
defer response.Body.Close()
194+
195+
s, err := utils.ReadAndCleanBody(response.Body)
196+
if err != nil {
197+
return "", fmt.Errorf("reading response: %w", err)
198+
}
199+
200+
if response.StatusCode != http.StatusOK {
201+
return "", fmt.Errorf("%w: %d: %s",
202+
errors.ErrHTTPStatusNotValid, response.StatusCode, utils.ToSingleLine(s))
203+
}
204+
205+
var parsedJSON struct {
206+
Data []struct {
207+
ID string `json:"id"`
208+
Type string `json:"type"`
209+
Name string `json:"name"`
210+
} `json:"data"`
211+
}
212+
213+
err = json.Unmarshal([]byte(s), &parsedJSON)
214+
if err != nil {
215+
return "", fmt.Errorf("%w: cannot parse json", errors.ErrReceivedNoResult)
216+
}
217+
218+
for _, subdomain := range parsedJSON.Data {
219+
if subdomain.Name == p.owner {
220+
return subdomain.ID, nil
221+
}
222+
}
223+
return "", fmt.Errorf("%w: domain not found", errors.ErrDomainNotFound)
224+
}

0 commit comments

Comments
 (0)