Skip to content

Commit

Permalink
Add DNS provider for West.cn/西部数码 (#2318)
Browse files Browse the repository at this point in the history
  • Loading branch information
ldez authored Nov 21, 2024
1 parent 6fccca6 commit b349021
Show file tree
Hide file tree
Showing 15 changed files with 905 additions and 4 deletions.
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

0 comments on commit b349021

Please sign in to comment.