Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add DNS provider for West.cn/西部数码 #2318

Merged
merged 5 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,13 +224,13 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns).
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/websupport/">Websupport</a></td>
<td><a href="https://go-acme.github.io/lego/dns/wedos/">WEDOS</a></td>
<td><a href="https://go-acme.github.io/lego/dns/westcn/">West.cn/西部数码</a></td>
<td><a href="https://go-acme.github.io/lego/dns/yandex360/">Yandex 360</a></td>
<td><a href="https://go-acme.github.io/lego/dns/yandexcloud/">Yandex Cloud</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/yandexcloud/">Yandex Cloud</a></td>
<td><a href="https://go-acme.github.io/lego/dns/yandex/">Yandex PDD</a></td>
<td><a href="https://go-acme.github.io/lego/dns/zoneee/">Zone.ee</a></td>
<td><a href="https://go-acme.github.io/lego/dns/zonomi/">Zonomi</a></td>
<td></td>
</tr></table>

<!-- END DNS PROVIDERS LIST -->
Expand Down
22 changes: 22 additions & 0 deletions cmd/zz_gen_cmd_dnshelp.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

69 changes: 69 additions & 0 deletions docs/content/dns/zz_gen_westcn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
title: "West.cn/西部数码"
date: 2019-03-03T16:39:46+01:00
draft: false
slug: westcn
dnsprovider:
since: "v4.21.0"
code: "westcn"
url: "https://www.west.cn"
---

<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/westcn/westcn.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->


Configuration for [West.cn/西部数码](https://www.west.cn).


<!--more-->

- Code: `westcn`
- Since: v4.21.0


Here is an example bash command using the West.cn/西部数码 provider:

```bash
WESTCN_USERNAME="xxx" \
WESTCN_PASSWORD="yyy" \
lego --email [email protected] --dns westcn -d '*.example.com' -d example.com run
```




## Credentials

| Environment Variable Name | Description |
|-----------------------|-------------|
| `WESTCN_PASSWORD` | API password |
| `WESTCN_USERNAME` | Username |

The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
More information [here]({{% ref "dns#configuration-and-credentials" %}}).


## Additional Configuration

| Environment Variable Name | Description |
|--------------------------------|-------------|
| `WESTCN_HTTP_TIMEOUT` | API request timeout |
| `WESTCN_POLLING_INTERVAL` | Time between DNS propagation check |
| `WESTCN_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation |
| `WESTCN_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]({{% ref "dns#configuration-and-credentials" %}}).




## More information

- [API documentation](https://www.west.cn/CustomerCenter/doc/domain_v2.html)

<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/westcn/westcn.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
2 changes: 1 addition & 1 deletion docs/data/zz_cli_help.toml
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ To display the documentation for a specific DNS provider, run:
$ lego dnshelp -c code

Supported DNS providers:
acme-dns, alidns, allinkl, arvancloud, auroradns, autodns, azure, azuredns, bindman, bluecat, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, conoha, constellix, corenetworks, cpanel, derak, desec, designate, digitalocean, directadmin, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dynu, easydns, edgedns, efficientip, epik, exec, exoscale, freemyip, gandi, gandiv5, gcloud, gcore, glesys, godaddy, googledomains, hetzner, hostingde, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ipv64, iwantmyname, joker, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manual, metaname, mijnhost, mittwald, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, netcup, netlify, nicmanager, nifcloud, njalla, nodion, ns1, oraclecloud, otc, ovh, pdns, plesk, porkbun, rackspace, rainyun, rcodezero, regfish, regru, rfc2136, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, selectelv2, selfhostde, servercow, shellrent, simply, sonic, stackpath, technitium, tencentcloud, timewebcloud, transip, ultradns, variomedia, vegadns, vercel, versio, vinyldns, vkcloud, volcengine, vscale, vultr, webnames, websupport, wedos, yandex, yandex360, yandexcloud, zoneee, zonomi
acme-dns, alidns, allinkl, arvancloud, auroradns, autodns, azure, azuredns, bindman, bluecat, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, conoha, constellix, corenetworks, cpanel, derak, desec, designate, digitalocean, directadmin, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dynu, easydns, edgedns, efficientip, epik, exec, exoscale, freemyip, gandi, gandiv5, gcloud, gcore, glesys, godaddy, googledomains, hetzner, hostingde, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ipv64, iwantmyname, joker, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manual, metaname, mijnhost, mittwald, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, netcup, netlify, nicmanager, nifcloud, njalla, nodion, ns1, oraclecloud, otc, ovh, pdns, plesk, porkbun, rackspace, rainyun, rcodezero, regfish, regru, rfc2136, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, selectelv2, selfhostde, servercow, shellrent, simply, sonic, stackpath, technitium, tencentcloud, timewebcloud, transip, ultradns, variomedia, vegadns, vercel, versio, vinyldns, vkcloud, volcengine, vscale, vultr, webnames, websupport, wedos, westcn, yandex, yandex360, yandexcloud, zoneee, zonomi

More information: https://go-acme.github.io/lego/dns
"""
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ require (
golang.org/x/crypto v0.28.0
golang.org/x/net v0.30.0
golang.org/x/oauth2 v0.23.0
golang.org/x/text v0.19.0
golang.org/x/time v0.7.0
google.golang.org/api v0.204.0
gopkg.in/ns1/ns1-go.v2 v2.12.2
Expand Down Expand Up @@ -198,7 +199,6 @@ require (
golang.org/x/mod v0.21.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.19.0 // indirect
golang.org/x/tools v0.25.0 // indirect
google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 // indirect
Expand Down
211 changes: 211 additions & 0 deletions providers/dns/westcn/internal/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package internal

import (
"bytes"
"context"
"crypto/md5"
"encoding/hex"
"encoding/json"
"errors"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"

querystring "github.com/google/go-querystring/query"
"github.com/nrdcg/mailinabox/errutils"
"golang.org/x/text/encoding"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform"
)

const defaultBaseURL = "https://api.west.cn/api/v2"

// Client the West.cn API client.
type Client struct {
username string
password string

encoder *encoding.Encoder

baseURL *url.URL
HTTPClient *http.Client
}

// NewClient creates a new Client.
func NewClient(username, password string) (*Client, error) {
if username == "" || password == "" {
return nil, errors.New("credentials missing")
}

baseURL, _ := url.Parse(defaultBaseURL)

return &Client{
username: username,
password: password,
encoder: simplifiedchinese.GBK.NewEncoder(),
baseURL: baseURL,
HTTPClient: &http.Client{Timeout: 10 * time.Second},
}, nil
}

// AddRecord adds a record.
// https://www.west.cn/CustomerCenter/doc/domain_v2.html#37u3001u6dfbu52a0u57dfu540du89e3u67900a3ca20id3d37u3001u6dfbu52a0u57dfu540du89e3u67903e203ca3e
func (c *Client) AddRecord(ctx context.Context, record Record) (int, error) {
values, err := querystring.Values(record)
if err != nil {
return 0, err
}

req, err := c.newRequest(ctx, "domain", "adddnsrecord", values)
if err != nil {
return 0, err
}

results := &APIResponse[RecordID]{}

err = c.do(req, results)
if err != nil {
return 0, err
}

if results.Result != http.StatusOK {
return 0, results
}

return results.Data.ID, nil
}

// DeleteRecord deleted a record.
// https://www.west.cn/CustomerCenter/doc/domain_v2.html#39u3001u5220u9664u57dfu540du89e3u67900a3ca20id3d39u3001u5220u9664u57dfu540du89e3u67903e203ca3e
func (c *Client) DeleteRecord(ctx context.Context, domain string, recordID int) error {
values := url.Values{}
values.Set("domain", domain)
values.Set("id", strconv.Itoa(recordID))

req, err := c.newRequest(ctx, "domain", "deldnsrecord", values)
if err != nil {
return err
}

results := &APIResponse[any]{}

err = c.do(req, results)
if err != nil {
return err
}

if results.Result != http.StatusOK {
return results
}

return nil
}

func (c *Client) newRequest(ctx context.Context, p, act string, form url.Values) (*http.Request, error) {
if form == nil {
form = url.Values{}
}

c.sign(form, time.Now())

values, err := c.convertURLValues(form)
if err != nil {
return nil, err
}

endpoint := c.baseURL.JoinPath(p, "/")

query := endpoint.Query()
query.Set("act", act)
endpoint.RawQuery = query.Encode()

req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), strings.NewReader(values.Encode()))
if err != nil {
return nil, err
}

req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

return req, nil
}

func (c *Client) sign(form url.Values, now time.Time) {
timestamp := strconv.FormatInt(now.UnixMilli(), 10)

sum := md5.Sum([]byte(c.username + c.password + timestamp))

form.Set("token", hex.EncodeToString(sum[:]))
form.Set("username", c.username)
form.Set("time", timestamp)
}

func (c *Client) do(req *http.Request, result any) error {
resp, err := c.HTTPClient.Do(req)
if err != nil {
return err
}

defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusOK {
return parseError(req, resp)
}

if result == nil {
return nil
}

raw, err := io.ReadAll(resp.Body)
if err != nil {
return errutils.NewReadResponseError(req, resp.StatusCode, err)
}

err = gbkDecoder(raw).Decode(result)
if err != nil {
return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
}

return nil
}

func (c *Client) convertURLValues(values url.Values) (url.Values, error) {
results := make(url.Values)

for key, vs := range values {
encKey, err := c.encoder.String(key)
if err != nil {
return nil, err
}

for _, value := range vs {
encValue, err := c.encoder.String(value)
if err != nil {
return nil, err
}

results.Add(encKey, encValue)
}
}

return results, nil
}

func parseError(req *http.Request, resp *http.Response) error {
raw, _ := io.ReadAll(resp.Body)

result := &APIResponse[any]{}

err := gbkDecoder(raw).Decode(result)
if err != nil {
return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
}

return result
}

func gbkDecoder(raw []byte) *json.Decoder {
return json.NewDecoder(transform.NewReader(bytes.NewBuffer(raw), simplifiedchinese.GBK.NewDecoder()))
}
Loading