Showing preview only (396K chars total). Download the full file or copy to clipboard to get everything.
Repository: jeessy2/ddns-go
Branch: master
Commit: 345d436ede27
Files: 115
Total size: 369.7 KB
Directory structure:
gitextract_dlov5rlz/
├── .editorconfig
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ ├── config.yml
│ │ └── feature_request.yml
│ ├── dependabot.yml
│ ├── pull_request_template.md
│ └── workflows/
│ ├── dockerhub-description.yml
│ ├── release.yml
│ ├── stale.yml
│ └── test.yml
├── .gitignore
├── .goreleaser.yml
├── .vscode/
│ └── launch.json
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── README_EN.md
├── config/
│ ├── config.go
│ ├── domains.go
│ ├── domains_test.go
│ ├── netInterface.go
│ ├── netInterface_test.go
│ ├── user.go
│ ├── webhook.go
│ └── webhook_test.go
├── dns/
│ ├── alidns.go
│ ├── aliesa.go
│ ├── baidu.go
│ ├── callback.go
│ ├── cloudflare.go
│ ├── dnsla.go
│ ├── dnspod.go
│ ├── dynadot.go
│ ├── dynv6.go
│ ├── edgeone.go
│ ├── eranet.go
│ ├── gcore.go
│ ├── godaddy.go
│ ├── huawei.go
│ ├── index.go
│ ├── name_com.go
│ ├── namecheap.go
│ ├── namesilo.go
│ ├── nowcn.go
│ ├── nsone.go
│ ├── porkbun.go
│ ├── rainyun.go
│ ├── spaceship.go
│ ├── tencent_cloud.go
│ ├── traffic_route.go
│ └── vercel.go
├── go.mod
├── main.go
├── static/
│ ├── common.css
│ ├── constant.js
│ ├── i18n.js
│ ├── theme-button.css
│ ├── theme.js
│ ├── tooltips.js
│ └── utils.js
├── util/
│ ├── aliyun_signer.go
│ ├── aliyun_signer_util.go
│ ├── andriod_time.go
│ ├── baidu_signer.go
│ ├── bcrypt.go
│ ├── copy_url_params.go
│ ├── docker_util.go
│ ├── escape.go
│ ├── http_client_util.go
│ ├── http_util.go
│ ├── huawei_signer.go
│ ├── ip_cache.go
│ ├── messages.go
│ ├── net.go
│ ├── net_resolver.go
│ ├── net_resolver_test.go
│ ├── net_test.go
│ ├── ordinal.go
│ ├── ordinal_test.go
│ ├── osutil/
│ │ ├── daemon_unix.go
│ │ └── daemon_win32.go
│ ├── semver/
│ │ ├── version.go
│ │ └── version_test.go
│ ├── string.go
│ ├── string_test.go
│ ├── tencent_cloud_signer.go
│ ├── termux.go
│ ├── termux_test.go
│ ├── token.go
│ ├── traffic_route_signer.go
│ ├── update/
│ │ ├── apply.go
│ │ ├── apply_test.go
│ │ ├── arch.go
│ │ ├── arm.go
│ │ ├── decompress.go
│ │ ├── decompress_test.go
│ │ ├── detect.go
│ │ ├── errors.go
│ │ ├── latest.go
│ │ ├── package.go
│ │ ├── release.go
│ │ └── update.go
│ ├── user.go
│ └── wait_internet.go
└── web/
├── auth.go
├── login.go
├── login.html
├── logout.go
├── logs.go
├── return_json.go
├── save.go
├── webhookTest.go
├── writing.go
└── writing.html
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
# http://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.go]
indent_style = tab
indent_size = 2
[Dockerfile]
indent_style = tab
indent_size = 4
[Makefile]
indent_style = tab
indent_size = 4
[.travis.yml]
indent_style = space
indent_size = 2
[*.json]
indent_style = space
indent_size = 2
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: Bug
description: Report a bug in ddns-go
labels: ['bug']
body:
- type: textarea
attributes:
label: Description
description: A clear and concise description of what the bug is
validations:
required: true
- type: dropdown
attributes:
label: DNS Provider
description: The DNS provider you are using
multiple: true
options:
- 阿里云
- 腾讯云
- DnsPod
- Cloudflare
- 华为云
- Callback
- 百度云
- Porkbun
- GoDaddy
- Namecheap
- NameSilo
- Vercel
- Dynadot
- Others
- type: dropdown
attributes:
label: Did you search for similar issues before submitting this one?
options:
- No, I didn't
- Yes, I did, but I didn't find anything useful
validations:
required: true
- type: dropdown
attributes:
label: Operating System
description: The operating system you are running ddns-go on
options:
- Linux
- Windows
- macOS (Darwin)
- FreeBSD
validations:
required: true
- type: dropdown
attributes:
label: Architecture
description: The architecture you are running ddns-go on
options:
- i386
- x86_64
- armv5
- armv6
- armv7
- arm64
- mips
- mipsle
- mips64
- mips64le
validations:
required: true
- type: input
attributes:
label: Version
description: The version of ddns-go you are using
placeholder: v0.0.1
validations:
required: true
- type: dropdown
attributes:
label: How are you running ddns-go?
options:
- Docker
- Service
- Other
validations:
required: true
- type: textarea
attributes:
label: Any other information
description: |
Please provide the steps to reproduce the bug.
Or any other screenshots or logs that might help us understand the issue better.
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
- name: None of the above?
url: https://github.com/jeessy2/ddns-go/discussions
about: If you have any other questions, please visit our Discussions page
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: Feature Request
description: Feature request for ddns-go
labels: ['enhancement']
body:
- type: textarea
attributes:
label: Description
description: A clear and concise description of what the feature is
validations:
required: true
- type: textarea
attributes:
label: Problem
description: Describe the problem you are facing
- type: textarea
attributes:
label: Other Description
description: Any other information you would like to provide
- type: checkboxes
attributes:
label: Checklist
description: Please check the following before submitting your feature request
options:
- label: I am using the latest version and have confirmed that the feature is not yet implemented in the latest version
required: true
- label: I have searched for similar feature requests before submitting this one
required: true
================================================
FILE: .github/dependabot.yml
================================================
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "gomod" # Golang
directory: "/" # Location of package manifests
schedule:
interval: "daily"
- package-ecosystem: "github-actions" # GitHub Actions
directory: "/"
schedule:
interval: "daily"
================================================
FILE: .github/pull_request_template.md
================================================
# What does this PR do?
# Motivation
# Additional Notes
================================================
FILE: .github/workflows/dockerhub-description.yml
================================================
name: Update Docker Hub Description
on:
push:
branches:
- master
paths:
- README.md
- .github/workflows/dockerhub-description.yml
jobs:
dockerHubDescription:
runs-on: ubuntu-latest
if: github.repository == 'jeessy2/ddns-go'
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Docker Hub Description
uses: peter-evans/dockerhub-description@v5
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
repository: ${{ secrets.DOCKER_USERNAME }}/ddns-go
short-description: ${{ github.event.repository.description }}
================================================
FILE: .github/workflows/release.yml
================================================
name: release
on:
push:
# Sequence of patterns matched against refs/tags
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
permissions:
contents: write
packages: write
jobs:
goreleaser:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v7
if: startsWith(github.ref, 'refs/tags/')
with:
distribution: goreleaser
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
================================================
FILE: .github/workflows/stale.yml
================================================
name: Close stale issues and PRs
on:
schedule:
- cron: "30 1 * * *"
jobs:
stale:
permissions:
issues: write # for actions/stale to close stale issues
pull-requests: write # for actions/stale to close stale PRs
runs-on: ubuntu-latest
steps:
- name: Stale
uses: actions/stale@v10
with:
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove Stale label or comment or this will be closed in 5 days.'
stale-pr-message: 'This PR is stale because it has been open 30 days with no activity. Remove Stale label or comment or this will be closed in 5 days.'
close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.'
close-pr-message: 'This PR was closed because it has been stalled for 5 days with no activity.'
exempt-issue-labels: 'bug,help wanted,question,documentation,keep'
exempt-pr-labels: 'bug,help wanted,question,documentation,keep'
days-before-stale: 30
days-before-close: 5
================================================
FILE: .github/workflows/test.yml
================================================
name: test
on:
push:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
goarch: [amd64, arm64, riscv64]
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
- name: Test
run: |
# Run tests only when GOARCH is amd64, otherwize run builds only.
if [ "${{ matrix.goarch }}" = "amd64" ]; then
make build test
else
GOARCH=${{ matrix.goarch }} make build
fi
- name: Upload artifact
uses: actions/upload-artifact@v7
with:
name: ddns-go_${{ matrix.goarch }}
path: ddns-go
================================================
FILE: .gitignore
================================================
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
/ddns-go
__*
# Folders
_obj
_test
.vagrant
releases
tmp
/.idea/
vendor/
/dist
# Architecture specific extensions/prefixes
trace.out
*.out
.DS_Store
_testmain.go
*.exe
*.test
*.prof
profile.cov
coverage.html
/go.sum
# Emacs backup files
*~
.*~
================================================
FILE: .goreleaser.yml
================================================
# This is an example goreleaser.yaml file with some sane defaults.
# Make sure to check the documentation at http://goreleaser.com
version: 2
before:
hooks:
# You may remove this if you don't use go modules.
- go mod download
# you may remove this if you don't need go generate
- go generate ./...
builds:
- env:
- CGO_ENABLED=0
flags:
- -trimpath
goos:
- android
- linux
- windows
- darwin
- freebsd
goarch:
- '386'
- amd64
- arm
- arm64
- mips
- mipsle
- mips64
- mips64le
- riscv64
goarm:
- '5'
- '6'
- '7'
gomips:
- hardfloat
- softfloat
ignore:
# we only need the arm64 build on android
- goos: android
goarch: arm
- goos: android
goarch: '386'
- goos: android
goarch: amd64
ldflags:
- -s -w -X main.version={{.Tag}} -X main.buildTime={{.Date}}
hooks:
post:
- sh -c 'test -d zoneinfo || cp -r /usr/share/zoneinfo .'
archives:
# use zip for windows archives
- format_overrides:
- goos: windows
format: zip
# this name template makes the OS and Arch compatible with the results of uname.
name_template: >-
{{ .ProjectName }}_
{{- .Version }}_
{{- .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Mips }}_{{ .Mips }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
checksum:
name_template: 'checksums.txt'
snapshot:
version_template: "{{ incpatch .Version }}-devel"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
dockers:
- image_templates:
- "{{ .Env.DOCKER_USERNAME }}/ddns-go:{{ .Tag }}-amd64"
- "ghcr.io/{{ tolower .Env.GITHUB_REPOSITORY }}:{{ .Tag }}-amd64"
use: buildx
extra_files:
- zoneinfo
build_flag_templates:
- "--platform=linux/amd64"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- image_templates:
- "{{ .Env.DOCKER_USERNAME }}/ddns-go:{{ .Tag }}-arm64"
- "ghcr.io/{{ tolower .Env.GITHUB_REPOSITORY }}:{{ .Tag }}-arm64"
use: buildx
extra_files:
- zoneinfo
build_flag_templates:
- "--platform=linux/arm64"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
goarch: arm64
- image_templates:
- "{{ .Env.DOCKER_USERNAME }}/ddns-go:{{ .Tag }}-armv7"
- "ghcr.io/{{ tolower .Env.GITHUB_REPOSITORY }}:{{ .Tag }}-armv7"
use: buildx
extra_files:
- zoneinfo
build_flag_templates:
- "--platform=linux/arm/v7"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
goarch: arm
goarm: 7
- image_templates:
- "{{ .Env.DOCKER_USERNAME }}/ddns-go:{{ .Tag }}-riscv64"
- "ghcr.io/{{ tolower .Env.GITHUB_REPOSITORY }}:{{ .Tag }}-riscv64"
use: buildx
extra_files:
- zoneinfo
build_flag_templates:
- "--platform=linux/riscv64"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
goarch: riscv64
docker_manifests:
- name_template: "{{ .Env.DOCKER_USERNAME }}/ddns-go:{{ .Tag }}"
image_templates:
- "{{ .Env.DOCKER_USERNAME }}/ddns-go:{{ .Tag }}-amd64"
- "{{ .Env.DOCKER_USERNAME }}/ddns-go:{{ .Tag }}-arm64"
- "{{ .Env.DOCKER_USERNAME }}/ddns-go:{{ .Tag }}-armv7"
- "{{ .Env.DOCKER_USERNAME }}/ddns-go:{{ .Tag }}-riscv64"
- name_template: "{{ .Env.DOCKER_USERNAME }}/ddns-go:latest"
image_templates:
- "{{ .Env.DOCKER_USERNAME }}/ddns-go:{{ .Tag }}-amd64"
- "{{ .Env.DOCKER_USERNAME }}/ddns-go:{{ .Tag }}-arm64"
- "{{ .Env.DOCKER_USERNAME }}/ddns-go:{{ .Tag }}-armv7"
- "{{ .Env.DOCKER_USERNAME }}/ddns-go:{{ .Tag }}-riscv64"
- name_template: "ghcr.io/{{ tolower .Env.GITHUB_REPOSITORY }}:{{ .Tag }}"
image_templates:
- "ghcr.io/{{ tolower .Env.GITHUB_REPOSITORY }}:{{ .Tag }}-amd64"
- "ghcr.io/{{ tolower .Env.GITHUB_REPOSITORY }}:{{ .Tag }}-arm64"
- "ghcr.io/{{ tolower .Env.GITHUB_REPOSITORY }}:{{ .Tag }}-armv7"
- "ghcr.io/{{ tolower .Env.GITHUB_REPOSITORY }}:{{ .Tag }}-riscv64"
- name_template: "ghcr.io/{{ tolower .Env.GITHUB_REPOSITORY }}:latest"
image_templates:
- "ghcr.io/{{ tolower .Env.GITHUB_REPOSITORY }}:{{ .Tag }}-amd64"
- "ghcr.io/{{ tolower .Env.GITHUB_REPOSITORY }}:{{ .Tag }}-arm64"
- "ghcr.io/{{ tolower .Env.GITHUB_REPOSITORY }}:{{ .Tag }}-armv7"
- "ghcr.io/{{ tolower .Env.GITHUB_REPOSITORY }}:{{ .Tag }}-riscv64"
================================================
FILE: .vscode/launch.json
================================================
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/main.go",
"env": {},
"args": []
}
]
}
================================================
FILE: Dockerfile
================================================
FROM alpine
LABEL name=ddns-go
LABEL url=https://github.com/jeessy2/ddns-go
RUN apk add --no-cache curl grep
WORKDIR /app
COPY ddns-go /app/
COPY zoneinfo /usr/share/zoneinfo
ENV TZ=Asia/Shanghai
EXPOSE 9876
ENTRYPOINT ["/app/ddns-go"]
CMD ["-l", ":9876", "-f", "300"]
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2020 jeessy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Makefile
================================================
.PHONY: build clean test test-race
# 如果找不到 tag 则使用 HEAD commit
VERSION=$(shell git describe --tags `git rev-list --tags --max-count=1` 2>/dev/null || git rev-parse --short HEAD)
BUILD_TIME=$(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
BIN=ddns-go
DIR_SRC=.
DOCKER_ENV=DOCKER_BUILDKIT=1
DOCKER=$(DOCKER_ENV) docker
GO_ENV=CGO_ENABLED=0
GO_FLAGS=-ldflags="-X main.version=$(VERSION) -X 'main.buildTime=$(BUILD_TIME)' -extldflags -static -s -w" -trimpath
GO=$(GO_ENV) $(shell which go)
GOROOT=$(shell `which go` env GOROOT)
GOPATH=$(shell `which go` env GOPATH)
build: $(DIR_SRC)/main.go
@$(GO) build $(GO_FLAGS) -o $(BIN) $(DIR_SRC)
build_docker_image:
@$(DOCKER) build -f ./Dockerfile -t ddns-go:$(VERSION) .
test:
@$(GO) test ./...
test-race:
@$(GO) test -race ./...
# clean all build result
clean:
@$(GO) clean ./...
@rm -f $(BIN)
@rm -rf ./dist/*
================================================
FILE: README.md
================================================
# DDNS-GO
[ ](https://github.com/jeessy2/ddns-go/releases/latest) [](https://github.com/jeessy2/ddns-go/blob/master/go.mod) [](https://goreportcard.com/report/github.com/jeessy2/ddns-go/v6) [](https://registry.hub.docker.com/r/jeessy/ddns-go) [](https://registry.hub.docker.com/r/jeessy/ddns-go)
中文 | [English](https://github.com/jeessy2/ddns-go/blob/master/README_EN.md)
自动获得你的公网 IPv4 或 IPv6 地址,并解析到对应的域名服务。
- [特性](#特性)
- [系统中使用](#系统中使用)
- [Docker中使用](#docker中使用)
- [使用IPv6](#使用ipv6)
- [Webhook](#webhook)
- [Callback](#callback)
- [界面](#界面)
- [开发&自行编译](#开发自行编译)
## 特性
- 支持Mac、Windows、Linux系统,支持ARM、x86、RISC-V架构
- 支持的域名服务商 `阿里云` `阿里云 ESA` `腾讯云` `Dnspod` `Cloudflare` `华为云` `Callback` `百度云` `Porkbun` `GoDaddy` `Namecheap` `NameSilo` `Dynadot` `DNSLA` `时代互联` `Eranet` `Gcore` `IBM NS1 Connect` `雨云`
- 支持接口/网卡/[命令](https://github.com/jeessy2/ddns-go/wiki/通过命令获取IP参考)获取IP
- 支持以服务的方式运行
- 默认间隔5分钟同步一次
- 支持同时配置多个DNS服务商
- 支持多个域名同时解析
- 支持多级域名
- 网页中配置,简单又方便,默认勾选`禁止从公网访问`
- 网页中方便快速查看最近50条日志
- 支持Webhook通知
- 支持TTL
- 支持部分DNS服务商[传递自定义参数](https://github.com/jeessy2/ddns-go/wiki/传递自定义参数),实现地域解析/多IP等功能
> [!NOTE]
> 建议在启用公网访问时,使用 Nginx 等反向代理软件启用 HTTPS 访问,以保证安全性。[FAQ](https://github.com/jeessy2/ddns-go/wiki/FAQ)
## 系统中使用
- 从 [Releases](https://github.com/jeessy2/ddns-go/releases) 下载并解压 ddns-go
- 安装服务
- Mac/Linux: `sudo ./ddns-go -s install`
- Win(以管理员打开cmd): `.\ddns-go.exe -s install`
- 配置
- 打开浏览器并访问`http://localhost:9876`进行初始化配置
- [可选] 服务卸载
- Mac/Linux: `sudo ./ddns-go -s uninstall`
- Win(以管理员打开cmd): `.\ddns-go.exe -s uninstall`
- [可选] 支持安装带参数
- `-l` 监听地址
- `-f` 同步间隔时间(秒)
- `-cacheTimes` 间隔N次与服务商比对
- `-c` 自定义配置文件路径
- `-noweb` 不启动web服务
- `-skipVerify` 跳过证书验证
- `-dns` 自定义 DNS 服务器
- `-resetPassword` 重置密码
- [可选] 参考示例
- 10分钟同步一次, 并指定了配置文件地址
```bash
./ddns-go -s install -f 600 -c /Users/name/.ddns_go_config.yaml
```
- 每 10 秒检查一次本地 IP 变化, 每 30 分钟对比一下 IP 变化, 实现 IP 变化即时触发更新且不会被服务商限流, 如果使用接口获取IP, 需要注意接口限流
```bash
./ddns-go -s install -f 10 -cacheTimes 180
```
- 重置密码
```bash
./ddns-go -resetPassword 123456
./ddns-go -resetPassword 123456 -c /Users/name/.ddns_go_config.yaml
```
## Docker中使用
- 挂载主机目录, 使用docker host模式。可把 `/opt/ddns-go` 替换为你主机任意目录, 配置文件为隐藏文件
```bash
docker run -d --name ddns-go --restart=always --net=host -v /opt/ddns-go:/root jeessy/ddns-go
```
- 打开浏览器并访问`http://Docker主机IP:9876`进行初始化配置
- [可选] 使用 `ghcr.io` 镜像
```bash
docker run -d --name ddns-go --restart=always --net=host -v /opt/ddns-go:/root ghcr.io/jeessy2/ddns-go
```
- [可选] 支持启动带参数 `-l`监听地址 `-f`间隔时间(秒)
```bash
docker run -d --name ddns-go --restart=always --net=host -v /opt/ddns-go:/root jeessy/ddns-go -l :9877 -f 600
```
- [可选] 不使用docker host模式
```bash
docker run -d --name ddns-go --restart=always -p 9876:9876 -v /opt/ddns-go:/root jeessy/ddns-go
```
- [可选] 重置密码
```bash
docker exec ddns-go ./ddns-go -resetPassword 123456
docker restart ddns-go
```
## 使用IPv6
- 前提:你的电脑或终端能正常获取IPv6,并能正常访问IPv6
- Windows/Mac:推荐 [系统中使用](#系统中使用),Windows/Mac桌面版的docker不支持`--net=host`
- 群晖:
- 套件中心下载docker并打开
- 注册表中搜索`ddns-go`并下载
- 映像 -> 选择`jeessy/ddns-go` -> 启动 -> 高级设置 -> 网络中勾选`使用与 Docker Host 相同的网络`,高级设置中勾选`启动自动重新启动`
- 在浏览器中打开`http://群晖IP:9876`,修改你的配置,成功
- Linux的x86或arm架构,推荐使用Docker的`--net=host`模式。参考 [Docker中使用](#Docker中使用)
- 虚拟机中使用有可能正常获取IPv6,但不能正常访问IPv6
## Webhook
- 支持webhook, 域名更新成功或不成功时, 会回调填写的URL
- 支持的变量
| 变量名 | 说明 |
| ---- | ---- |
| #{ipv4Addr} | 新的IPv4地址 |
| #{ipv4Result} | IPv4地址更新结果: `未改变` `失败` `成功`|
| #{ipv4Domains} | IPv4的域名,多个以`,`分割 |
| #{ipv6Addr} | 新的IPv6地址 |
| #{ipv6Result} | IPv6地址更新结果: `未改变` `失败` `成功`|
| #{ipv6Domains} | IPv6的域名,多个以`,`分割 |
- 如 RequestBody 为空则为 GET 请求,否则为 POST 请求
- <details><summary>Server酱</summary>
```
https://sctapi.ftqq.com/[SendKey].send?title=你的公网IP变了&desp=主人IPv4变了#{ipv4Addr},域名更新结果:#{ipv4Result}
```
- <details><summary>Bark</summary>
```
https://api.day.app/[YOUR_KEY]/主人IPv4变了#{ipv4Addr},域名更新结果:#{ipv4Result}
```
</details>
- <details><summary>钉钉</summary>
- 钉钉电脑端 -> 群设置 -> 智能群助手 -> 添加机器人 -> 自定义
- 只勾选 `自定义关键词`, 输入的关键字必须包含在RequestBody的content中, 如:`你的公网IP变了`
- URL中输入钉钉给你的 `Webhook地址`
- RequestBody中输入
```json
{
"msgtype": "markdown",
"markdown": {
"title": "你的公网IP变了",
"text": "#### 你的公网IP变了 \n - IPv4地址:#{ipv4Addr} \n - 域名更新结果:#{ipv4Result} \n"
}
}
```
</details>
- <details><summary>飞书</summary>
- 飞书电脑端 -> 群设置 -> 添加机器人 -> 自定义机器人
- 安全设置只勾选 `自定义关键词`, 输入的关键字必须包含在RequestBody的content中, 如:`你的公网IP变了`
- URL中输入飞书给你的 `Webhook地址`
- RequestBody中输入
```json
{
"msg_type": "post",
"content": {
"post": {
"zh_cn": {
"title": "你的公网IP变了",
"content": [
[
{
"tag": "text",
"text": "IPv4地址:#{ipv4Addr}"
}
],
[
{
"tag": "text",
"text": "域名更新结果:#{ipv4Result}"
}
]
]
}
}
}
}
```
</details>
- <details><summary>Telegram</summary>
[ddns-telegram-bot](https://github.com/WingLim/ddns-telegram-bot)
</details>
- <details><summary>plusplus 推送加</summary>
- [获取token](https://www.pushplus.plus/push1.html)
- URL中输入 `https://www.pushplus.plus/send`
- RequestBody中输入
```json
{
"token": "your token",
"title": "你的公网IP变了",
"content": "你的公网IP变了 \n - IPv4地址:#{ipv4Addr} \n - 域名更新结果:#{ipv4Result} \n"
}
```
</details>
- <details><summary>Discord</summary>
- Discord任意客户端 -> 伺服器 -> 频道设置 -> 整合 -> 查看Webhook -> 新Webhook -> 复制Webhook网址
- URL中输入Discord复制的 `Webhook网址`
- RequestBody中输入
```json
{
"content": "域名 #{ipv4Domains} 动态解析 #{ipv4Result}.",
"embeds": [
{
"description": "#{ipv4Domains} 的动态解析 #{ipv4Result}, IP: #{ipv4Addr}",
"color": 15258703,
"author": {
"name": "DDNS"
},
"footer": {
"text": "DDNS #{ipv4Result}"
}
}
]
}
```
</details>
- [查看更多Webhook配置参考](https://github.com/jeessy2/ddns-go/issues/327)
## Callback
- 通过自定义回调可支持更多的第三方DNS服务商
- 配置的域名有几行, 就会回调几次
- 支持的变量
| 变量名 | 说明 |
| ---- | ---- |
| #{ip} | 新的IPv4/IPv6地址 |
| #{domain} | 当前域名 |
| #{recordType} | 记录类型 `A`或`AAAA` |
| #{ttl} | TTL |
- 如 RequestBody 为空则为 GET 请求,否则为 POST 请求
- [Callback配置参考](https://github.com/jeessy2/ddns-go/wiki/Callback配置参考)
## 界面

## 开发&自行编译
- 如果喜欢从源代码编译自己的版本,可以使用本项目提供的 Makefile 构建
- 使用 `make build` 生成本地编译后的 `ddns-go` 可执行文件
- 使用 `make build_docker_image` 自行编译 Docker 镜像
================================================
FILE: README_EN.md
================================================
# DDNS-GO
[ ](https://github.com/jeessy2/ddns-go/releases/latest) [](https://github.com/jeessy2/ddns-go/blob/master/go.mod) [](https://goreportcard.com/report/github.com/jeessy2/ddns-go/v6) [](https://registry.hub.docker.com/r/jeessy/ddns-go) [](https://registry.hub.docker.com/r/jeessy/ddns-go)
[中文](https://github.com/jeessy2/ddns-go/blob/master/README.md) | English
Automatically obtain your public IPv4 or IPv6 address and resolve it to the corresponding domain name service.
- [Features](#Features)
- [Use in system](#Use-in-system)
- [Use in docker](#Use-in-docker)
- [Webhook](#webhook)
- [Callback](#callback)
- [Web interfaces](#Web-interfaces)
## Features
- Support Mac, Windows, Linux system, support ARM, x86, RISC-V architecture
- Support domain service providers `Aliyun` `Aliyun ESA` `Tencent` `Dnspod` `Cloudflare` `Huawei` `Callback` `Baidu` `Porkbun` `GoDaddy` `Namecheap` `NameSilo` `Dynadot` `DNSLA` `Nowcn` `Eranet` `Gcore` `IBM NS1 Connect` `Rainyun`
- Support interface / netcard / command to get IP
- Support running as a service
- Default interval is 5 minutes
- Support configuring multiple DNS service providers at the same time
- Support multiple domain name resolution at the same time
- Support multi-level domain name
- Configured on the web page, simple and convenient
- In the web page, you can quickly view the latest 50 logs
- Support Webhook notification
- Support TTL
- Support for some domain service providers to pass [custom parameters](https://github.com/jeessy2/ddns-go/wiki/传递自定义参数) to achieve multi-IP and other functions
> [!NOTE]
> If you enable public network access, it is recommended to use Nginx and other reverse proxy software to enable HTTPS access to ensure security.
## Use in system
- Download and unzip ddns-go from [Releases](https://github.com/jeessy2/ddns-go/releases)
- Run in service mode
- Mac/Linux: `sudo ./ddns-go -s install`
- Win(Run as administrator): `.\ddns-go.exe -s install`
- Config
- Please open the browser and visit `http://localhost:9876` for initial configuration
- [Optional] Uninstall service
- Mac/Linux: `sudo ./ddns-go -s uninstall`
- Win(Run as administrator): `.\ddns-go.exe -s uninstall`
- [Optional] Support installation with parameters
- `-l` listen address
- `-f` sync frequency(seconds)
- `-cacheTimes` interval N times compared with service providers
- `-c` custom configuration file path
- `-noweb` does not start web service
- `-skipVerify` skip certificate verification
- `-dns` custom DNS server
- `-resetPassword` reset password
- [Optional] Examples
- 10 minutes to synchronize once, and the configuration file address is specified
```bash
./ddns-go -s install -f 600 -c /Users/name/.ddns_go_config.yaml
```
- Every 10 seconds to check the local IP changes, every 30 minutes to compare the IP changes, to achieve IP changes immediately trigger updates and will not be limited by the service providers, if the use of api to obtain IP, need to pay attention to the api side of the flow limit
```bash
./ddns-go -s install -f 10 -cacheTimes 180
```
- reset password
```bash
./ddns-go -resetPassword 123456
```
## Use in docker
- Mount the host directory, use the docker host mode. You can replace `/opt/ddns-go` with any directory on your host, the configuration file is a hidden file
```bash
docker run -d --name ddns-go --restart=always --net=host -v /opt/ddns-go:/root jeessy/ddns-go
```
- Please open the browser and visit `http://DOCKER_IP:9876` for initial configuration
- [Optional] Use `ghcr.io` mirror
```bash
docker run -d --name ddns-go --restart=always --net=host -v /opt/ddns-go:/root ghcr.io/jeessy2/ddns-go
```
- [Optional] Support startup with parameters `-l`listen address `-f`Sync frequency(seconds)
```bash
docker run -d --name ddns-go --restart=always --net=host -v /opt/ddns-go:/root jeessy/ddns-go -l :9877 -f 600
```
- [Optional] Without using docker host mode
```bash
docker run -d --name ddns-go --restart=always -p 9876:9876 -v /opt/ddns-go:/root jeessy/ddns-go
```
- [Optional] Reset password
```bash
docker exec ddns-go ./ddns-go -resetPassword 123456
docker restart ddns-go
```
## Webhook
- Support webhook, when the domain name is updated successfully or not, the URL filled in will be called back
- Support variables
| Variable name | Comments |
| ---- | ---- |
| #{ipv4Addr} | The new IPv4 |
| #{ipv4Result} | IPv4 update result: `no changed` `success` `failed`|
| #{ipv4Domains} | IPv4 domains,Split by `,` |
| #{ipv6Addr} | The new IPv6 |
| #{ipv6Result} | IPv6 update result: `no changed` `success` `failed`|
| #{ipv6Domains} | IPv6 domains,Split by `,` |
- If RequestBody is empty, it is a `GET` request, otherwise it is a `POST` request
- <details><summary>Telegram</summary>
[ddns-telegram-bot](https://github.com/WingLim/ddns-telegram-bot)
</details>
- <details><summary>Discord</summary>
- Discord client -> Server -> Channel Settings -> Integration -> View Webhook -> New Webhook -> Copy Webhook URL
- Input the `Webhook URL` copied from Discord in the URL
- Input in RequestBody
```json
{
"content": "The domain name #{ipv4Domains} dynamically resolves to #{ipv4Result}.",
"embeds": [
{
"description": "Domains: #{ipv4Domains}, Result: #{ipv4Result}, IP: #{ipv4Addr}",
"color": 15258703,
"author": {
"name": "DDNS"
},
"footer": {
"text": "DDNS #{ipv4Result}"
}
}
]
}
```
</details>
- [More webhook configuration reference](https://github.com/jeessy2/ddns-go/issues/327)
## Callback
- Support more third-party DNS service providers through custom callback
- Callback will be called as many times as there are lines in the configured domain name
- Support variables
| Variable name | Comments |
| ---- | ---- |
| #{ip} | The new IPv4/IPv6 address|
| #{domain} | Current domain |
| #{recordType} | Record type `A` or `AAAA` |
| #{ttl} | TTL |
- If RequestBody is empty, it is a `GET` request, otherwise it is a `POST` request
## Web interfaces

================================================
FILE: config/config.go
================================================
package config
import (
"errors"
"io"
"log"
"net/http"
"os"
"os/exec"
"regexp"
"runtime"
"strconv"
"strings"
"sync"
"github.com/jeessy2/ddns-go/v6/util"
passwordvalidator "github.com/wagslane/go-password-validator"
"gopkg.in/yaml.v3"
)
// Ipv4Reg IPv4正则
var Ipv4Reg = regexp.MustCompile(`((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])`)
// Ipv6Reg IPv6正则
var Ipv6Reg = regexp.MustCompile(`((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))`)
// DnsConfig 配置
type DnsConfig struct {
Name string
Ipv4 struct {
Enable bool
// 获取IP类型 url/netInterface
GetType string
URL string
NetInterface string
Cmd string
Domains []string
}
Ipv6 struct {
Enable bool
// 获取IP类型 url/netInterface
GetType string
URL string
NetInterface string
Cmd string
Ipv6Reg string // ipv6匹配正则表达式
Domains []string
}
DNS DNS
TTL string
// 发送HTTP请求时使用的网卡名称,为空则使用默认网卡
HttpInterface string
}
// DNS DNS配置
type DNS struct {
// 名称。如:alidns,webhook
Name string
ID string
Secret string
// ExtParam 扩展参数,用于某些DNS提供商的特殊需求(如Vercel的teamId)
ExtParam string
}
type Config struct {
DnsConf []DnsConfig
User
Webhook
// 禁止公网访问
NotAllowWanAccess bool
// 语言
Lang string
}
// ConfigCache ConfigCache
type cacheType struct {
ConfigSingle *Config
Err error
Lock sync.Mutex
}
var cache = &cacheType{}
// GetConfigCached 获得缓存的配置
func GetConfigCached() (conf Config, err error) {
cache.Lock.Lock()
defer cache.Lock.Unlock()
if cache.ConfigSingle != nil {
return *cache.ConfigSingle, cache.Err
}
// init config
cache.ConfigSingle = &Config{}
configFilePath := util.GetConfigFilePath()
_, err = os.Stat(configFilePath)
if err != nil {
cache.Err = err
return *cache.ConfigSingle, err
}
byt, err := os.ReadFile(configFilePath)
if err != nil {
util.Log("异常信息: %s", err)
cache.Err = err
return *cache.ConfigSingle, err
}
err = yaml.Unmarshal(byt, cache.ConfigSingle)
if err != nil {
util.Log("异常信息: %s", err)
cache.Err = err
return *cache.ConfigSingle, err
}
// 未填写登录信息, 确保不能从公网访问
if cache.ConfigSingle.Username == "" && cache.ConfigSingle.Password == "" {
cache.ConfigSingle.NotAllowWanAccess = true
}
// remove err
cache.Err = nil
return *cache.ConfigSingle, err
}
// CompatibleConfig 兼容之前的配置文件
func (conf *Config) CompatibleConfig() {
// 如果之前密码不为空且不是bcrypt加密后的密码, 把密码加密并保存
if conf.Password != "" && !util.IsHashedPassword(conf.Password) {
hashedPwd, err := util.HashPassword(conf.Password)
if err == nil {
conf.Password = hashedPwd
conf.SaveConfig()
}
}
// 兼容v5.0.0之前的配置文件
if len(conf.DnsConf) > 0 {
return
}
configFilePath := util.GetConfigFilePath()
_, err := os.Stat(configFilePath)
if err != nil {
return
}
byt, err := os.ReadFile(configFilePath)
if err != nil {
return
}
dnsConf := &DnsConfig{}
err = yaml.Unmarshal(byt, dnsConf)
if err != nil {
return
}
if len(dnsConf.DNS.Name) > 0 {
cache.Lock.Lock()
defer cache.Lock.Unlock()
conf.DnsConf = append(conf.DnsConf, *dnsConf)
cache.ConfigSingle = conf
}
}
// SaveConfig 保存配置
func (conf *Config) SaveConfig() (err error) {
cache.Lock.Lock()
defer cache.Lock.Unlock()
byt, err := yaml.Marshal(conf)
if err != nil {
log.Println(err)
return err
}
configFilePath := util.GetConfigFilePath()
err = os.WriteFile(configFilePath, byt, 0600)
if err != nil {
log.Println(err)
return
}
util.Log("配置文件已保存在: %s", configFilePath)
// 清空配置缓存
cache.ConfigSingle = nil
return
}
// 重置密码
func (conf *Config) ResetPassword(newPassword string) {
// 初始化语言
util.InitLogLang(conf.Lang)
// 先检查密码是否安全
hashedPwd, err := conf.CheckPassword(newPassword)
if err != nil {
util.Log(err.Error())
return
}
// 保存配置
conf.Password = hashedPwd
conf.SaveConfig()
util.Log("用户名 %s 的密码已重置成功! 请重启ddns-go", conf.Username)
}
// CheckPassword 检查密码
func (conf *Config) CheckPassword(newPassword string) (hashedPwd string, err error) {
var minEntropyBits float64 = 30
if conf.NotAllowWanAccess {
minEntropyBits = 25
}
err = passwordvalidator.Validate(newPassword, minEntropyBits)
if err != nil {
return "", errors.New(util.LogStr("密码不安全!尝试使用更复杂的密码"))
}
// 加密密码
hashedPwd, err = util.HashPassword(newPassword)
if err != nil {
return "", errors.New(util.LogStr("异常信息: %s", err.Error()))
}
return
}
func (conf *DnsConfig) getIpv4AddrFromInterface() string {
ipv4, _, err := GetNetInterface()
if err != nil {
util.Log("从网卡获得IPv4失败")
return ""
}
for _, netInterface := range ipv4 {
if netInterface.Name == conf.Ipv4.NetInterface && len(netInterface.Address) > 0 {
return netInterface.Address[0]
}
}
util.Log("从网卡中获得IPv4失败! 网卡名: %s", conf.Ipv4.NetInterface)
return ""
}
func (conf *DnsConfig) getIpv4AddrFromUrl() string {
client := util.CreateNoProxyHTTPClient("tcp4")
urls := strings.Split(conf.Ipv4.URL, ",")
for _, url := range urls {
url = strings.TrimSpace(url)
resp, err := client.Get(url)
if err != nil {
util.Log("通过接口获取IPv4失败! 接口地址: %s", url)
util.Log("异常信息: %s", err)
continue
}
defer resp.Body.Close()
lr := io.LimitReader(resp.Body, 1024000)
body, err := io.ReadAll(lr)
if err != nil {
util.Log("异常信息: %s", err)
continue
}
result := Ipv4Reg.FindString(string(body))
if result == "" {
util.Log("获取IPv4结果失败! 接口: %s ,返回值: %s", url, string(body))
}
return result
}
return ""
}
func (conf *DnsConfig) getAddrFromCmd(addrType string) string {
var cmd string
var comp *regexp.Regexp
if addrType == "IPv4" {
cmd = conf.Ipv4.Cmd
comp = Ipv4Reg
} else {
cmd = conf.Ipv6.Cmd
comp = Ipv6Reg
}
// cmd is empty
if cmd == "" {
return ""
}
// run cmd with proper shell
var execCmd *exec.Cmd
if runtime.GOOS == "windows" {
execCmd = exec.Command("powershell", "-Command", cmd)
} else {
// If Bash does not exist, use sh
_, err := exec.LookPath("bash")
if err != nil {
execCmd = exec.Command("sh", "-c", cmd)
} else {
execCmd = exec.Command("bash", "-c", cmd)
}
}
// run cmd
out, err := execCmd.CombinedOutput()
if err != nil {
util.Log("获取%s结果失败! 未能成功执行命令:%s, 错误:%q, 退出状态码:%s", addrType, execCmd.String(), out, err)
return ""
}
str := string(out)
// get result
result := comp.FindString(str)
if result == "" {
util.Log("获取%s结果失败! 命令: %s, 标准输出: %q", addrType, execCmd.String(), str)
}
return result
}
// GetIpv4Addr 获得IPv4地址
func (conf *DnsConfig) GetIpv4Addr() string {
// 判断从哪里获取IP
switch conf.Ipv4.GetType {
case "netInterface":
// 从网卡获取 IP
return conf.getIpv4AddrFromInterface()
case "url":
// 从 URL 获取 IP
return conf.getIpv4AddrFromUrl()
case "cmd":
// 从命令行获取 IP
return conf.getAddrFromCmd("IPv4")
default:
log.Println("IPv4's get IP method is unknown")
return "" // unknown type
}
}
func (conf *DnsConfig) getIpv6AddrFromInterface() string {
_, ipv6, err := GetNetInterface()
if err != nil {
util.Log("从网卡获得IPv6失败")
return ""
}
for _, netInterface := range ipv6 {
if netInterface.Name == conf.Ipv6.NetInterface && len(netInterface.Address) > 0 {
if conf.Ipv6.Ipv6Reg != "" {
// 匹配第几个IPv6
if match, err := regexp.MatchString("@\\d", conf.Ipv6.Ipv6Reg); err == nil && match {
num, err := strconv.Atoi(conf.Ipv6.Ipv6Reg[1:])
if err == nil {
if num > 0 {
if num <= len(netInterface.Address) {
return netInterface.Address[num-1]
}
util.Log("未找到第 %d 个IPv6地址! 将使用第一个IPv6地址", num)
return netInterface.Address[0]
}
util.Log("IPv6匹配表达式 %s 不正确! 最小从1开始", conf.Ipv6.Ipv6Reg)
return ""
}
}
// 正则表达式匹配
util.Log("IPv6将使用正则表达式 %s 进行匹配", conf.Ipv6.Ipv6Reg)
for i := 0; i < len(netInterface.Address); i++ {
matched, err := regexp.MatchString(conf.Ipv6.Ipv6Reg, netInterface.Address[i])
if matched && err == nil {
util.Log("匹配成功! 匹配到地址: %s", netInterface.Address[i])
return netInterface.Address[i]
}
}
util.Log("没有匹配到任何一个IPv6地址, 将使用第一个地址")
}
return netInterface.Address[0]
}
}
util.Log("从网卡中获得IPv6失败! 网卡名: %s", conf.Ipv6.NetInterface)
return ""
}
func (conf *DnsConfig) getIpv6AddrFromUrl() string {
client := util.CreateNoProxyHTTPClient("tcp6")
urls := strings.Split(conf.Ipv6.URL, ",")
for _, url := range urls {
url = strings.TrimSpace(url)
resp, err := client.Get(url)
if err != nil {
util.Log("通过接口获取IPv6失败! 接口地址: %s", url)
util.Log("异常信息: %s", err)
continue
}
defer resp.Body.Close()
lr := io.LimitReader(resp.Body, 1024000)
body, err := io.ReadAll(lr)
if err != nil {
util.Log("异常信息: %s", err)
continue
}
result := Ipv6Reg.FindString(string(body))
if result == "" {
util.Log("获取IPv6结果失败! 接口: %s ,返回值: %s", url, result)
}
return result
}
return ""
}
// GetIpv6Addr 获得IPv6地址
func (conf *DnsConfig) GetIpv6Addr() (result string) {
// 判断从哪里获取IP
switch conf.Ipv6.GetType {
case "netInterface":
// 从网卡获取 IP
return conf.getIpv6AddrFromInterface()
case "url":
// 从 URL 获取 IP
return conf.getIpv6AddrFromUrl()
case "cmd":
// 从命令行获取 IP
return conf.getAddrFromCmd("IPv6")
default:
log.Println("IPv6's get IP method is unknown")
return "" // unknown type
}
}
// GetHTTPClient 获得HTTP客户端,如果配置了HttpInterface则绑定到指定网卡
func (conf *DnsConfig) GetHTTPClient() *http.Client {
return util.CreateHTTPClientWithInterface(conf.HttpInterface)
}
================================================
FILE: config/domains.go
================================================
package config
import (
"net/url"
"strings"
"github.com/jeessy2/ddns-go/v6/util"
"golang.org/x/net/idna"
"golang.org/x/net/publicsuffix"
)
// Domains Ipv4/Ipv6 domains
type Domains struct {
Ipv4Addr string
Ipv4Cache *util.IpCache
Ipv4Domains []*Domain
Ipv6Addr string
Ipv6Cache *util.IpCache
Ipv6Domains []*Domain
}
// Domain 域名实体
type Domain struct {
// DomainName 根域名
DomainName string
// SubDomain 子域名
SubDomain string
CustomParams string
UpdateStatus updateStatusType // 更新状态
}
// DomainTuples 域名元组映射 key: Domain.String()
type DomainTuples map[string]*DomainTuple
// DomainTuple 域名元组
type DomainTuple struct {
RecordType string
// Primary 首要域名 Domains[-1] = Primary
Primary *Domain
Domains []*Domain
IpAddrs []string
Ipv4Addr string
Ipv6Addr string
}
// nontransitionalLookup implements the nontransitional processing as specified in
// Unicode Technical Standard 46 with almost all checkings off to maximize user freedom.
//
// Copied from: https://github.com/cloudflare/cloudflare-go/blob/v0.97.0/dns.go#L95
var nontransitionalLookup = idna.New(
idna.MapForLookup(),
idna.StrictDomainName(false),
idna.ValidateLabels(false),
)
func (d Domain) String() string {
if d.SubDomain != "" {
return d.SubDomain + "." + d.DomainName
}
return d.DomainName
}
// GetFullDomain 获得全部的,子域名
func (d Domain) GetFullDomain() string {
if d.SubDomain != "" {
return d.SubDomain + "." + d.DomainName
}
return "@" + "." + d.DomainName
}
// GetSubDomain 获得子域名,为空返回@
// 阿里云/腾讯云/dnspod/GoDaddy/namecheap 需要
func (d Domain) GetSubDomain() string {
if d.SubDomain != "" {
return d.SubDomain
}
return "@"
}
// GetCustomParams not be nil
func (d Domain) GetCustomParams() url.Values {
if d.CustomParams != "" {
q, err := url.ParseQuery(d.CustomParams)
if err == nil {
return q
}
}
return url.Values{}
}
// ToASCII converts [Domain] to its ASCII form,
// using non-transitional process specified in UTS 46.
//
// Note: conversion errors are silently discarded and partial conversion
// results are used.
func (d Domain) ToASCII() string {
name, _ := nontransitionalLookup.ToASCII(d.String())
return name
}
// GetNewIp 接口/网卡/命令获得 ip 并校验用户输入的域名
func (domains *Domains) GetNewIp(dnsConf *DnsConfig) {
domains.Ipv4Domains = checkParseDomains(dnsConf.Ipv4.Domains)
domains.Ipv6Domains = checkParseDomains(dnsConf.Ipv6.Domains)
// IPv4
if dnsConf.Ipv4.Enable && len(domains.Ipv4Domains) > 0 {
ipv4Addr := dnsConf.GetIpv4Addr()
if ipv4Addr != "" {
domains.Ipv4Addr = ipv4Addr
domains.Ipv4Cache.TimesFailedIP = 0
} else {
// 启用IPv4 & 未获取到IP & 填写了域名 & 失败刚好3次,防止偶尔的网络连接失败,并且只发一次
domains.Ipv4Cache.TimesFailedIP++
if domains.Ipv4Cache.TimesFailedIP == 3 {
domains.Ipv4Domains[0].UpdateStatus = UpdatedFailed
}
util.Log("未能获取IPv4地址, 将不会更新")
}
}
// IPv6
if dnsConf.Ipv6.Enable && len(domains.Ipv6Domains) > 0 {
ipv6Addr := dnsConf.GetIpv6Addr()
if ipv6Addr != "" {
domains.Ipv6Addr = ipv6Addr
domains.Ipv6Cache.TimesFailedIP = 0
} else {
// 启用IPv6 & 未获取到IP & 填写了域名 & 失败刚好3次,防止偶尔的网络连接失败,并且只发一次
domains.Ipv6Cache.TimesFailedIP++
if domains.Ipv6Cache.TimesFailedIP == 3 {
domains.Ipv6Domains[0].UpdateStatus = UpdatedFailed
}
util.Log("未能获取IPv6地址, 将不会更新")
}
}
}
// checkParseDomains 校验并解析用户输入的域名
func checkParseDomains(domainArr []string) (domains []*Domain) {
for _, domainStr := range domainArr {
domainStr = strings.TrimSpace(domainStr)
if domainStr == "" {
continue
}
domain := &Domain{}
// qp(queryParts) 从域名中提取自定义参数,如 baidu.com?q=1 => [baidu.com, q=1]
qp := strings.Split(domainStr, "?")
domainStr = qp[0]
// dp(domainParts) 将域名(qp[0])分割为子域名与根域名,如 www:example.cn.eu.org => [www, example.cn.eu.org]
dp := strings.Split(domainStr, ":")
switch len(dp) {
case 1: // 不使用冒号分割,自动识别域名
domainName, err := publicsuffix.EffectiveTLDPlusOne(domainStr)
if err != nil {
util.Log("域名: %s 不正确", domainStr)
util.Log("异常信息: %s", err)
continue
}
domain.DomainName = domainName
domainLen := len(domainStr) - len(domainName) - 1
if domainLen > 0 {
domain.SubDomain = domainStr[:domainLen]
}
case 2: // 使用冒号分隔,为 子域名:根域名 格式
sp := strings.Split(dp[1], ".")
if len(sp) <= 1 {
util.Log("域名: %s 不正确", domainStr)
continue
}
domain.DomainName = dp[1]
domain.SubDomain = dp[0]
default:
util.Log("域名: %s 不正确", domainStr)
continue
}
// 参数条件
if len(qp) == 2 {
u, err := url.Parse("https://baidu.com?" + qp[1])
if err != nil {
util.Log("域名: %s 解析失败", domainStr)
continue
}
domain.CustomParams = u.Query().Encode()
}
domains = append(domains, domain)
}
return
}
// GetNewIpResult 获得GetNewIp结果
func (domains *Domains) GetNewIpResult(recordType string) (ipAddr string, retDomains []*Domain) {
if recordType == "AAAA" {
if domains.Ipv6Cache.Check(domains.Ipv6Addr) {
return domains.Ipv6Addr, domains.Ipv6Domains
} else {
util.Log("IPv6未改变, 将等待 %d 次后与DNS服务商进行比对", domains.Ipv6Cache.Times)
return "", domains.Ipv6Domains
}
}
// IPv4
if domains.Ipv4Cache.Check(domains.Ipv4Addr) {
return domains.Ipv4Addr, domains.Ipv4Domains
} else {
util.Log("IPv4未改变, 将等待 %d 次后与DNS服务商进行比对", domains.Ipv4Cache.Times)
return "", domains.Ipv4Domains
}
}
// GetAllNewIpResult 获得getNewIp结果
func (domains *Domains) GetAllNewIpResult(multiRecordType string) (results DomainTuples) {
ipv4Addr, ipv4Domains := domains.GetNewIpResult("A")
ipv6Addr, ipv6Domains := domains.GetNewIpResult("AAAA")
if ipv4Addr == "" && ipv6Addr == "" {
return
}
cap := 0
if ipv4Addr != "" {
cap += len(ipv4Domains)
}
if ipv6Addr != "" {
cap += len(ipv6Domains)
}
results = make(DomainTuples, cap)
results.append(ipv4Addr, ipv4Domains, multiRecordType, DomainTuple{RecordType: "A", Ipv4Addr: domains.Ipv4Addr, Ipv6Addr: domains.Ipv6Addr})
results.append(ipv6Addr, ipv6Domains, multiRecordType, DomainTuple{RecordType: "AAAA", Ipv4Addr: domains.Ipv4Addr, Ipv6Addr: domains.Ipv6Addr})
return
}
// append 添加域名到域名元组映射
func (domains DomainTuples) append(ipAddr string, retDomains []*Domain, multiRecordType string, template DomainTuple) {
if ipAddr == "" {
return
}
for _, domain := range retDomains {
domainStr := domain.String()
if tuple, ok := domains[domainStr]; ok {
if tuple.RecordType != template.RecordType {
tuple.RecordType = multiRecordType
}
tuple.Primary = domain
tuple.Domains = append(tuple.Domains, domain)
tuple.IpAddrs = append(tuple.IpAddrs, ipAddr)
} else {
tuple := template
domains[domainStr] = &tuple
tuple.Primary = domain
tuple.Domains = []*Domain{domain}
tuple.IpAddrs = []string{ipAddr}
}
}
}
// SetUpdateStatus 设置更新状态
func (d *DomainTuple) SetUpdateStatus(status updateStatusType) {
if d.Primary.UpdateStatus == status {
return
}
for _, domain := range d.Domains {
domain.UpdateStatus = status
}
}
// GetIpAddrPool 设置更新状态
func (d *DomainTuple) GetIpAddrPool(separator string) (result string) {
s := d.Primary.GetCustomParams().Get("IpAddrPool")
if len(s) != 0 {
return strings.NewReplacer(
"{ipv4Addr}", d.Ipv4Addr,
"{ipv6Addr}", d.Ipv6Addr,
).Replace(s)
}
switch d.RecordType {
case "A":
return d.Ipv4Addr
case "AAAA":
return d.Ipv6Addr
default:
return d.Ipv4Addr + separator + d.Ipv6Addr
}
}
================================================
FILE: config/domains_test.go
================================================
package config
import "testing"
// TestToASCII test converts the name of [Domain] to its ASCII form.
//
// Copied from: https://github.com/cloudflare/cloudflare-go/blob/v0.97.0/dns_test.go#L15
func TestToASCII(t *testing.T) {
tests := map[string]struct {
domain string
expected string
}{
"empty": {
"", "",
},
"unicode get encoded": {
"😺.com", "xn--138h.com",
},
"unicode gets mapped and encoded": {
"ÖBB.at", "xn--bb-eka.at",
},
"punycode stays punycode": {
"xn--138h.com", "xn--138h.com",
},
"hyphens are not checked": {
"s3--s4.com", "s3--s4.com",
},
"STD3 rules are not enforced": {
"℀.com", "a/c.com",
},
"bidi check is disabled": {
"englishﻋﺮﺑﻲ.com", "xn--english-gqjzfwd1j.com",
},
"invalid joiners are allowed": {
"a\u200cb.com", "xn--ab-j1t.com",
},
"partial results are used despite errors": {
"xn--:D.xn--.😺.com", "xn--:d..xn--138h.com",
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
d := &Domain{DomainName: tt.domain}
actual := d.ToASCII()
if actual != tt.expected {
t.Errorf("ToASCII() = %v, want %v", actual, tt.expected)
}
})
}
}
// TestParseDomainArr 测试 parseDomainArr
func TestParseDomainArr(t *testing.T) {
domains := []string{"mydomain.com", "test.mydomain.com", "test2.test.mydomain.com", "mydomain.com.mydomain.com", "mydomain.com.cn",
"test.mydomain.com.cn", "test:mydomain.com.cn",
"test.mydomain.com?Line=oversea&RecordId=123", "test.mydomain.com.cn?Line=oversea&RecordId=123",
"test2:test.mydomain.com?Line=oversea&RecordId=123"}
result := []Domain{
{DomainName: "mydomain.com", SubDomain: ""},
{DomainName: "mydomain.com", SubDomain: "test"},
{DomainName: "mydomain.com", SubDomain: "test2.test"},
{DomainName: "mydomain.com", SubDomain: "mydomain.com"},
{DomainName: "mydomain.com.cn", SubDomain: ""},
{DomainName: "mydomain.com.cn", SubDomain: "test"},
{DomainName: "mydomain.com.cn", SubDomain: "test"},
{DomainName: "mydomain.com", SubDomain: "test", CustomParams: "Line=oversea&RecordId=123"},
{DomainName: "mydomain.com.cn", SubDomain: "test", CustomParams: "Line=oversea&RecordId=123"},
{DomainName: "test.mydomain.com", SubDomain: "test2", CustomParams: "Line=oversea&RecordId=123"},
}
parsedDomains := checkParseDomains(domains)
for i := 0; i < len(parsedDomains); i++ {
if parsedDomains[i].DomainName != result[i].DomainName ||
parsedDomains[i].SubDomain != result[i].SubDomain ||
parsedDomains[i].CustomParams != result[i].CustomParams {
t.Errorf("解析 %s 失败:\n期待 DomainName:%s,得到 DomainName:%s\n期待 SubDomain:%s,得到 SubDomain:%s\n期待 CustomParams:%s,得到 CustomParams:%s",
parsedDomains[i].String(),
result[i].DomainName, parsedDomains[i].DomainName,
result[i].SubDomain, parsedDomains[i].SubDomain,
result[i].CustomParams, parsedDomains[i].CustomParams)
}
}
}
================================================
FILE: config/netInterface.go
================================================
package config
import (
"fmt"
"net"
)
// NetInterface 本机网络
type NetInterface struct {
Name string
Address []string
}
// GetNetInterface 获得网卡地址
// 返回ipv4, ipv6地址
func GetNetInterface() (ipv4NetInterfaces []NetInterface, ipv6NetInterfaces []NetInterface, err error) {
allNetInterfaces, err := net.Interfaces()
if err != nil {
fmt.Println("net.Interfaces failed, err:", err.Error())
return ipv4NetInterfaces, ipv6NetInterfaces, err
}
// https://en.wikipedia.org/wiki/IPv6_address#General_allocation
_, ipv6Unicast, _ := net.ParseCIDR("2000::/3")
for i := 0; i < len(allNetInterfaces); i++ {
if (allNetInterfaces[i].Flags & net.FlagUp) != 0 {
addrs, _ := allNetInterfaces[i].Addrs()
ipv4 := []string{}
ipv6 := []string{}
for _, address := range addrs {
if ipnet, ok := address.(*net.IPNet); ok && ipnet.IP.IsGlobalUnicast() {
_, bits := ipnet.Mask.Size()
// 需匹配全局单播地址
if bits == 128 && ipv6Unicast.Contains(ipnet.IP) {
ipv6 = append(ipv6, ipnet.IP.String())
}
if bits == 32 {
ipv4 = append(ipv4, ipnet.IP.String())
}
}
}
if len(ipv4) > 0 {
ipv4NetInterfaces = append(
ipv4NetInterfaces,
NetInterface{
Name: allNetInterfaces[i].Name,
Address: ipv4,
},
)
}
if len(ipv6) > 0 {
ipv6NetInterfaces = append(
ipv6NetInterfaces,
NetInterface{
Name: allNetInterfaces[i].Name,
Address: ipv6,
},
)
}
}
}
return ipv4NetInterfaces, ipv6NetInterfaces, nil
}
================================================
FILE: config/netInterface_test.go
================================================
package config
import (
"testing"
)
func TestGetNetInterface(t *testing.T) {
ipv4NetInterfaces, ipv6NetInterfaces, err := GetNetInterface()
if err != nil {
t.Error(err)
}
t.Log(ipv4NetInterfaces, ipv6NetInterfaces)
}
================================================
FILE: config/user.go
================================================
package config
// User 登录用户
type User struct {
Username string
Password string
}
================================================
FILE: config/webhook.go
================================================
package config
import (
"encoding/json"
"net/http"
"net/url"
"strings"
"github.com/jeessy2/ddns-go/v6/util"
)
// Webhook Webhook
type Webhook struct {
WebhookURL string
WebhookRequestBody string
WebhookHeaders string
}
// updateStatusType 更新状态
type updateStatusType string
const (
// UpdatedNothing 未改变
UpdatedNothing updateStatusType = "未改变"
// UpdatedFailed 更新失败
UpdatedFailed = "失败"
// UpdatedSuccess 更新成功
UpdatedSuccess = "成功"
)
// 更新失败次数
var updatedFailedTimes = 0
// hasJSONPrefix returns true if the string starts with a JSON open brace.
func hasJSONPrefix(s string) bool {
return strings.HasPrefix(s, "{") || strings.HasPrefix(s, "[")
}
// ExecWebhook 添加或更新IPv4/IPv6记录, 返回是否有更新失败的
func ExecWebhook(domains *Domains, conf *Config) (v4Status updateStatusType, v6Status updateStatusType) {
v4Status = getDomainsStatus(domains.Ipv4Domains)
v6Status = getDomainsStatus(domains.Ipv6Domains)
if conf.WebhookURL != "" && (v4Status != UpdatedNothing || v6Status != UpdatedNothing) {
// 第3次失败才触发一次webhook
if v4Status == UpdatedFailed || v6Status == UpdatedFailed {
updatedFailedTimes++
if updatedFailedTimes != 3 {
util.Log("将不会触发Webhook, 仅在第 3 次失败时触发一次Webhook, 当前失败次数:%d", updatedFailedTimes)
return
}
} else {
updatedFailedTimes = 0
}
// 成功和失败都要触发webhook
method := "GET"
postPara := ""
contentType := "application/x-www-form-urlencoded"
if conf.WebhookRequestBody != "" {
method = "POST"
postPara = replacePara(domains, conf.WebhookRequestBody, v4Status, v6Status)
if json.Valid([]byte(postPara)) {
contentType = "application/json"
} else if hasJSONPrefix(postPara) {
// 如果 RequestBody 的 JSON 无效但前缀为 JSON,提示无效
util.Log("Webhook中的 RequestBody JSON 无效")
}
}
requestURL := replacePara(domains, conf.WebhookURL, v4Status, v6Status)
u, err := url.Parse(requestURL)
if err != nil {
util.Log("Webhook配置中的URL不正确")
return
}
q, _ := url.ParseQuery(u.RawQuery)
u.RawQuery = q.Encode()
req, err := http.NewRequest(method, u.String(), strings.NewReader(postPara))
if err != nil {
util.Log("Webhook调用失败! 异常信息:%s", err)
return
}
headers := extractHeaders(conf.WebhookHeaders)
for key, value := range headers {
req.Header.Add(key, value)
}
req.Header.Add("content-type", contentType)
clt := util.CreateHTTPClient()
resp, err := clt.Do(req)
body, err := util.GetHTTPResponseOrg(resp, err)
if err == nil {
util.Log("Webhook调用成功! 返回数据:%s", string(body))
} else {
util.Log("Webhook调用失败! 异常信息:%s", err)
}
}
return
}
// getDomainsStatus 获取域名状态
func getDomainsStatus(domains []*Domain) updateStatusType {
successNum := 0
for _, v46 := range domains {
switch v46.UpdateStatus {
case UpdatedFailed:
// 一个失败,全部失败
return UpdatedFailed
case UpdatedSuccess:
successNum++
}
}
if successNum > 0 {
// 迭代完成后一个成功,就成功
return UpdatedSuccess
}
return UpdatedNothing
}
// replacePara 替换参数
func replacePara(domains *Domains, orgPara string, ipv4Result updateStatusType, ipv6Result updateStatusType) string {
return strings.NewReplacer(
"#{ipv4Addr}", domains.Ipv4Addr,
"#{ipv4Result}", util.LogStr(string(ipv4Result)), // i18n
"#{ipv4Domains}", getDomainsStr(domains.Ipv4Domains),
"#{ipv6Addr}", domains.Ipv6Addr,
"#{ipv6Result}", util.LogStr(string(ipv6Result)), // i18n
"#{ipv6Domains}", getDomainsStr(domains.Ipv6Domains),
).Replace(orgPara)
}
// getDomainsStr 用逗号分割域名
func getDomainsStr(domains []*Domain) string {
str := ""
for i, v46 := range domains {
str += v46.String()
if i != len(domains)-1 {
str += ","
}
}
return str
}
// extractHeaders converts s into a map of headers.
//
// See also: https://github.com/appleboy/gorush/blob/v1.17.0/notify/feedback.go#L15
func extractHeaders(s string) map[string]string {
lines := util.SplitLines(s)
headers := make(map[string]string, len(lines))
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
parts := strings.Split(line, ":")
if len(parts) != 2 {
util.Log("Webhook Header不正确: %s", line)
continue
}
k, v := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
headers[k] = v
}
return headers
}
================================================
FILE: config/webhook_test.go
================================================
package config
import (
"reflect"
"testing"
)
// TestExtractHeaders 测试 parseHeaderArr
func TestExtractHeaders(t *testing.T) {
input := `
a: foo
b: bar`
expected := map[string]string{
"a": "foo",
"b": "bar",
}
parsedHeaders := extractHeaders(input)
if !reflect.DeepEqual(parsedHeaders, expected) {
t.Errorf("Expected %v, got %v", expected, parsedHeaders)
}
}
================================================
FILE: dns/alidns.go
================================================
package dns
import (
"bytes"
"net/http"
"net/url"
"github.com/jeessy2/ddns-go/v6/config"
"github.com/jeessy2/ddns-go/v6/util"
)
const (
alidnsEndpoint string = "https://alidns.aliyuncs.com/"
)
// https://help.aliyun.com/document_detail/29776.html?spm=a2c4g.11186623.6.672.715a45caji9dMA
// Alidns Alidns
type Alidns struct {
DNS config.DNS
Domains config.Domains
TTL string
httpClient *http.Client
}
// AlidnsRecord record
type AlidnsRecord struct {
DomainName string
RecordID string
Value string
}
// AlidnsSubDomainRecords 记录
type AlidnsSubDomainRecords struct {
TotalCount int
DomainRecords struct {
Record []AlidnsRecord
}
}
// AlidnsResp 修改/添加返回结果
type AlidnsResp struct {
RecordID string
RequestID string
}
// Init 初始化
func (ali *Alidns) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpCache, ipv6cache *util.IpCache) {
ali.Domains.Ipv4Cache = ipv4cache
ali.Domains.Ipv6Cache = ipv6cache
ali.DNS = dnsConf.DNS
ali.Domains.GetNewIp(dnsConf)
if dnsConf.TTL == "" {
// 默认600s
ali.TTL = "600"
} else {
ali.TTL = dnsConf.TTL
}
ali.httpClient = dnsConf.GetHTTPClient()
}
// AddUpdateDomainRecords 添加或更新IPv4/IPv6记录
func (ali *Alidns) AddUpdateDomainRecords() config.Domains {
ali.addUpdateDomainRecords("A")
ali.addUpdateDomainRecords("AAAA")
return ali.Domains
}
func (ali *Alidns) addUpdateDomainRecords(recordType string) {
ipAddr, domains := ali.Domains.GetNewIpResult(recordType)
if ipAddr == "" {
return
}
for _, domain := range domains {
var records AlidnsSubDomainRecords
// 获取当前域名信息
params := domain.GetCustomParams()
params.Set("Action", "DescribeSubDomainRecords")
params.Set("DomainName", domain.DomainName)
params.Set("SubDomain", domain.GetFullDomain())
params.Set("Type", recordType)
err := ali.request(params, &records)
if err != nil {
util.Log("查询域名信息发生异常! %s", err)
domain.UpdateStatus = config.UpdatedFailed
return
}
if records.TotalCount > 0 {
// 默认第一个
recordSelected := records.DomainRecords.Record[0]
if params.Has("RecordId") {
for i := 0; i < len(records.DomainRecords.Record); i++ {
if records.DomainRecords.Record[i].RecordID == params.Get("RecordId") {
recordSelected = records.DomainRecords.Record[i]
}
}
}
// 存在,更新
ali.modify(recordSelected, domain, recordType, ipAddr)
} else {
// 不存在,创建
ali.create(domain, recordType, ipAddr)
}
}
}
// 创建
func (ali *Alidns) create(domain *config.Domain, recordType string, ipAddr string) {
params := domain.GetCustomParams()
params.Set("Action", "AddDomainRecord")
params.Set("DomainName", domain.DomainName)
params.Set("RR", domain.GetSubDomain())
params.Set("Type", recordType)
params.Set("Value", ipAddr)
params.Set("TTL", ali.TTL)
var result AlidnsResp
err := ali.request(params, &result)
if err != nil {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, err)
domain.UpdateStatus = config.UpdatedFailed
return
}
if result.RecordID != "" {
util.Log("新增域名解析 %s 成功! IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
} else {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, "返回RecordId为空")
domain.UpdateStatus = config.UpdatedFailed
}
}
// 修改
func (ali *Alidns) modify(recordSelected AlidnsRecord, domain *config.Domain, recordType string, ipAddr string) {
// 相同不修改
if recordSelected.Value == ipAddr {
util.Log("你的IP %s 没有变化, 域名 %s", ipAddr, domain)
return
}
params := domain.GetCustomParams()
params.Set("Action", "UpdateDomainRecord")
params.Set("RR", domain.GetSubDomain())
params.Set("RecordId", recordSelected.RecordID)
params.Set("Type", recordType)
params.Set("Value", ipAddr)
params.Set("TTL", ali.TTL)
var result AlidnsResp
err := ali.request(params, &result)
if err != nil {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err)
domain.UpdateStatus = config.UpdatedFailed
return
}
if result.RecordID != "" {
util.Log("更新域名解析 %s 成功! IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
} else {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, "返回RecordId为空")
domain.UpdateStatus = config.UpdatedFailed
}
}
// request 统一请求接口
func (ali *Alidns) request(params url.Values, result interface{}) (err error) {
method := http.MethodGet
util.AliyunSigner(ali.DNS.ID, ali.DNS.Secret, ¶ms, method, "2015-01-09")
req, err := http.NewRequest(
method,
alidnsEndpoint,
bytes.NewBuffer(nil),
)
req.URL.RawQuery = params.Encode()
if err != nil {
return
}
client := ali.httpClient
resp, err := client.Do(req)
err = util.GetHTTPResponse(resp, err, result)
return
}
================================================
FILE: dns/aliesa.go
================================================
package dns
import (
"bytes"
"encoding/json"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/jeessy2/ddns-go/v6/config"
"github.com/jeessy2/ddns-go/v6/util"
)
const (
aliesaEndpoint string = "https://esa.cn-hangzhou.aliyuncs.com/"
)
// Aliesa Aliesa
type Aliesa struct {
DNS config.DNS
Domains config.Domains
TTL string
siteCache map[string]AliesaSite
domainCache config.DomainTuples
httpClient *http.Client
}
// AliesaSiteResp 站点返回结果
type AliesaSiteResp struct {
TotalCount int
Sites []AliesaSite
}
// AliesaSites 站点
type AliesaSite struct {
SiteId int64
SiteName string
AccessType string
}
// AliesaRecordResp 记录返回结果
type AliesaRecordResp struct {
TotalCount int
Records []AliesaRecord
}
// AliesaRecord 记录
type AliesaRecord struct {
RecordId int64
RecordName string
Data struct {
Value string
}
}
// AliesaResp 修改/添加返回结果
type AliesaResp struct {
OriginPoolId int64 `json:"Id"`
RecordID int64
RequestID string
}
// Init 初始化
func (ali *Aliesa) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpCache, ipv6cache *util.IpCache) {
ali.Domains.Ipv4Cache = ipv4cache
ali.Domains.Ipv6Cache = ipv6cache
ali.DNS = dnsConf.DNS
ali.Domains.GetNewIp(dnsConf)
if dnsConf.TTL == "" {
// 默认600s
ali.TTL = "600"
} else {
ali.TTL = dnsConf.TTL
}
ali.httpClient = dnsConf.GetHTTPClient()
}
// AddUpdateDomainRecords 添加或更新IPv4/IPv6记录
func (ali *Aliesa) AddUpdateDomainRecords() config.Domains {
ali.siteCache = make(map[string]AliesaSite)
ali.domainCache = ali.Domains.GetAllNewIpResult("A/AAAA")
ali.addUpdateDomainRecords("A")
ali.addUpdateDomainRecords("AAAA")
ali.addUpdateDomainRecords("A/AAAA")
return ali.Domains
}
func (ali *Aliesa) addUpdateDomainRecords(recordType string) {
for _, domain := range ali.domainCache {
if domain.RecordType != recordType {
continue
}
// 获取站点
siteSelected, err := ali.getSite(domain)
if err != nil {
util.Log("查询域名信息发生异常! %s", err)
domain.SetUpdateStatus(config.UpdatedFailed)
return
}
if siteSelected.SiteId == 0 {
util.Log("在DNS服务商中未找到根域名: %s", domain.Primary.DomainName)
domain.SetUpdateStatus(config.UpdatedFailed)
return
}
// 处理源地址池
poolId, origins, err := ali.getOriginPool(siteSelected, domain)
if err != nil {
util.Log("查询域名信息发生异常! %s", err)
domain.SetUpdateStatus(config.UpdatedFailed)
return
}
// TODO:不允许相同ip
if len(origins) != 0 {
ali.updateOriginPool(siteSelected, domain, poolId, origins)
return
}
// 获取记录
recordSelected, err := ali.getRecord(siteSelected, domain, "A/AAAA")
if err != nil {
util.Log("查询域名信息发生异常! %s", err)
domain.SetUpdateStatus(config.UpdatedFailed)
return
}
if recordSelected.RecordId != 0 {
// 存在,更新
ali.modify(recordSelected, domain, "A/AAAA")
} else {
// 不存在,创建
ali.create(siteSelected, domain, "A/AAAA")
}
}
}
// 创建
// https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-createrecord
func (ali *Aliesa) create(site AliesaSite, domainTuple *config.DomainTuple, recordType string) {
domain := domainTuple.Primary
ipAddr := domainTuple.GetIpAddrPool(",")
params := domain.GetCustomParams()
params.Set("Action", "CreateRecord")
params.Set("SiteId", strconv.FormatInt(site.SiteId, 10))
params.Set("RecordName", domain.String())
params.Set("Type", recordType)
params.Set("Data", `{"Value":"`+ipAddr+`"}`)
params.Set("Ttl", ali.TTL)
// 兼容 CNAME 接入方式
if site.AccessType == "CNAME" && !params.Has("Proxied") {
params.Set("Proxied", "true")
}
if params.Has("Proxied") && !params.Has("BizName") {
params.Set("BizName", "web")
}
var result AliesaResp
err := ali.request(http.MethodPost, params, &result)
if err != nil {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, err)
domainTuple.SetUpdateStatus(config.UpdatedFailed)
return
}
if result.RecordID != 0 {
util.Log("新增域名解析 %s 成功! IP: %s", domain, ipAddr)
domainTuple.SetUpdateStatus(config.UpdatedSuccess)
} else {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, "返回RecordId为空")
domainTuple.SetUpdateStatus(config.UpdatedFailed)
}
}
// 修改
// https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-updaterecord
func (ali *Aliesa) modify(record AliesaRecord, domainTuple *config.DomainTuple, recordType string) {
domain := domainTuple.Primary
ipAddr := domainTuple.GetIpAddrPool(",")
// 相同不修改
if record.Data.Value == ipAddr {
util.Log("你的IP %s 没有变化, 域名 %s", ipAddr, domain)
return
}
params := domain.GetCustomParams()
params.Set("Action", "UpdateRecord")
params.Set("RecordId", strconv.FormatInt(record.RecordId, 10))
params.Set("Type", recordType)
params.Set("Data", `{"Value":"`+ipAddr+`"}`)
params.Set("Ttl", ali.TTL)
var result AliesaResp
err := ali.request(http.MethodPost, params, &result)
if err != nil {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err)
domainTuple.SetUpdateStatus(config.UpdatedFailed)
return
}
// 不检查 result.RecordID ,更新成功也会返回 0
util.Log("更新域名解析 %s 成功! IP: %s", domain, ipAddr)
domainTuple.SetUpdateStatus(config.UpdatedSuccess)
}
// 获取当前域名信息
// https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-listrecords
func (ali *Aliesa) getRecord(site AliesaSite, domainTuple *config.DomainTuple, recordType string) (result AliesaRecord, err error) {
domain := domainTuple.Primary
var recordResp AliesaRecordResp
params := url.Values{}
params.Set("Action", "ListRecords")
params.Set("SiteId", strconv.FormatInt(site.SiteId, 10))
params.Set("RecordName", domain.String())
params.Set("Type", recordType)
err = ali.request(http.MethodGet, params, &recordResp)
// recordResp.TotalCount == 0
if len(recordResp.Records) == 0 {
return
}
// 指定 RecordId
recordId := domain.GetCustomParams().Get("RecordId")
if recordId != "" {
for i := 0; i < len(recordResp.Records); i++ {
if strconv.FormatInt(recordResp.Records[i].RecordId, 10) == recordId {
return recordResp.Records[i], nil
}
}
}
return recordResp.Records[0], nil
}
// 获取域名的站点信息
// https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-listsites
func (ali *Aliesa) getSite(domainTuple *config.DomainTuple) (result AliesaSite, err error) {
domain := domainTuple.Primary
if site, ok := ali.siteCache[domain.DomainName]; ok {
return site, nil
}
// 解析自定义参数 SiteId,但不使用 api GetSite 查询
siteIdStr := domain.GetCustomParams().Get("SiteId")
if siteId, _ := strconv.ParseInt(siteIdStr, 10, 64); siteId != 0 {
// 兼容 CNAME 接入方式
result.AccessType = "CNAME"
result.SiteName = domain.DomainName
result.SiteId = siteId
return
}
var siteResp AliesaSiteResp
params := url.Values{}
params.Set("Action", "ListSites")
params.Set("SiteName", domain.DomainName)
err = ali.request(http.MethodGet, params, &siteResp)
if err != nil {
return
}
// siteResp.TotalCount == 0
if len(siteResp.Sites) == 0 {
return
}
result = siteResp.Sites[0]
ali.siteCache[domain.DomainName] = result
return
}
// getOriginPool 获取源地址池
// https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-listoriginpools
func (ali *Aliesa) getOriginPool(site AliesaSite, domainTuple *config.DomainTuple) (id int64, origins []map[string]interface{}, err error) {
name, found := strings.CutSuffix(domainTuple.Primary.SubDomain, ".origin-pool")
if !found {
return
}
params := url.Values{}
params.Set("Action", "ListOriginPools")
params.Set("SiteId", strconv.FormatInt(site.SiteId, 10))
params.Set("Name", name)
params.Set("MatchType", "exact")
result := struct {
TotalCount int
OriginPools []struct {
Id int64
Origins []map[string]interface{}
}
}{}
err = ali.request(http.MethodGet, params, &result)
if err == nil && len(result.OriginPools) > 0 {
pool := result.OriginPools[0]
id = pool.Id
origins = pool.Origins
}
return
}
// updateOriginPool 更新源地址池
// https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-updateoriginpool
func (ali *Aliesa) updateOriginPool(site AliesaSite, domainTuple *config.DomainTuple, id int64, origins []map[string]interface{}) {
needUpdate := false
count := len(domainTuple.Domains)
for _, origin := range origins {
// 源地址池不能有多个相同地址,因此 Domain 更少放内层
for i, d := range domainTuple.Domains {
name := d.GetCustomParams().Get("Name")
if origin["Name"] != name {
continue
}
// 相同不修改
address := domainTuple.IpAddrs[i]
if origin["Address"] != address {
origin["Address"] = address
needUpdate = true
}
count--
break
}
}
domain := domainTuple.Primary
ipAddr := domainTuple.GetIpAddrPool(",")
if count > 0 {
// 有新增的源地址
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, "不支持新增源地址")
domainTuple.SetUpdateStatus(config.UpdatedFailed)
return
}
if !needUpdate {
util.Log("你的IP %s 没有变化, 域名 %s", ipAddr, domain)
return
}
originsData, _ := json.Marshal(origins)
params := url.Values{}
params.Set("Action", "UpdateOriginPool")
params.Set("SiteId", strconv.FormatInt(site.SiteId, 10))
params.Set("Id", strconv.FormatInt(id, 10))
params.Set("Origins", string(originsData))
result := AliesaResp{}
err := ali.request(http.MethodPost, params, &result)
if err != nil {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err)
domainTuple.SetUpdateStatus(config.UpdatedFailed)
return
}
if result.OriginPoolId != 0 {
util.Log("更新域名解析 %s 成功! IP: %s", domain, ipAddr)
domainTuple.SetUpdateStatus(config.UpdatedSuccess)
} else {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, "返回 OriginPool Id为空")
domainTuple.SetUpdateStatus(config.UpdatedFailed)
}
}
// request 统一请求接口
func (ali *Aliesa) request(method string, params url.Values, result interface{}) (err error) {
util.AliyunSigner(ali.DNS.ID, ali.DNS.Secret, ¶ms, method, "2024-09-10")
req, err := http.NewRequest(
method,
aliesaEndpoint,
bytes.NewBuffer(nil),
)
req.URL.RawQuery = params.Encode()
if err != nil {
return
}
client := ali.httpClient
resp, err := client.Do(req)
err = util.GetHTTPResponse(resp, err, result)
return
}
================================================
FILE: dns/baidu.go
================================================
package dns
import (
"bytes"
"encoding/json"
"net/http"
"strconv"
"github.com/jeessy2/ddns-go/v6/config"
"github.com/jeessy2/ddns-go/v6/util"
)
// https://cloud.baidu.com/doc/BCD/s/4jwvymhs7
const (
baiduEndpoint = "https://bcd.baidubce.com"
)
type BaiduCloud struct {
DNS config.DNS
Domains config.Domains
TTL int
httpClient *http.Client
}
// BaiduRecord 单条解析记录
type BaiduRecord struct {
RecordId uint `json:"recordId"`
Domain string `json:"domain"`
View string `json:"view"`
Rdtype string `json:"rdtype"`
TTL int `json:"ttl"`
Rdata string `json:"rdata"`
ZoneName string `json:"zoneName"`
Status string `json:"status"`
}
// BaiduRecordsResp 获取解析列表拿到的结果
type BaiduRecordsResp struct {
TotalCount int `json:"totalCount"`
Result []BaiduRecord `json:"result"`
}
// BaiduListRequest 获取解析列表请求的body json
type BaiduListRequest struct {
Domain string `json:"domain"`
PageNum int `json:"pageNum"`
PageSize int `json:"pageSize"`
}
// BaiduModifyRequest 修改解析请求的body json
type BaiduModifyRequest struct {
RecordId uint `json:"recordId"`
Domain string `json:"domain"`
View string `json:"view"`
RdType string `json:"rdType"`
TTL int `json:"ttl"`
Rdata string `json:"rdata"`
ZoneName string `json:"zoneName"`
}
// BaiduCreateRequest 创建新解析请求的body json
type BaiduCreateRequest struct {
Domain string `json:"domain"`
RdType string `json:"rdType"`
TTL int `json:"ttl"`
Rdata string `json:"rdata"`
ZoneName string `json:"zoneName"`
}
func (baidu *BaiduCloud) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpCache, ipv6cache *util.IpCache) {
baidu.Domains.Ipv4Cache = ipv4cache
baidu.Domains.Ipv6Cache = ipv6cache
baidu.DNS = dnsConf.DNS
baidu.Domains.GetNewIp(dnsConf)
if dnsConf.TTL == "" {
// 默认300s
baidu.TTL = 300
} else {
ttl, err := strconv.Atoi(dnsConf.TTL)
if err != nil {
baidu.TTL = 300
} else {
baidu.TTL = ttl
}
}
baidu.httpClient = dnsConf.GetHTTPClient()
}
// AddUpdateDomainRecords 添加或更新IPv4/IPv6记录
func (baidu *BaiduCloud) AddUpdateDomainRecords() config.Domains {
baidu.addUpdateDomainRecords("A")
baidu.addUpdateDomainRecords("AAAA")
return baidu.Domains
}
func (baidu *BaiduCloud) addUpdateDomainRecords(recordType string) {
ipAddr, domains := baidu.Domains.GetNewIpResult(recordType)
if ipAddr == "" {
return
}
for _, domain := range domains {
var records BaiduRecordsResp
requestBody := BaiduListRequest{
Domain: domain.DomainName,
PageNum: 1,
PageSize: 1000,
}
err := baidu.request("POST", baiduEndpoint+"/v1/domain/resolve/list", requestBody, &records)
if err != nil {
util.Log("查询域名信息发生异常! %s", err)
domain.UpdateStatus = config.UpdatedFailed
return
}
find := false
for _, record := range records.Result {
if record.Domain == domain.GetSubDomain() {
//存在就去更新
baidu.modify(record, domain, recordType, ipAddr)
find = true
break
}
}
if !find {
//没找到,去创建
baidu.create(domain, recordType, ipAddr)
}
}
}
// create 创建新的解析
func (baidu *BaiduCloud) create(domain *config.Domain, recordType string, ipAddr string) {
var baiduCreateRequest = BaiduCreateRequest{
Domain: domain.GetSubDomain(), //处理一下@
RdType: recordType,
TTL: baidu.TTL,
Rdata: ipAddr,
ZoneName: domain.DomainName,
}
var result BaiduRecordsResp
err := baidu.request("POST", baiduEndpoint+"/v1/domain/resolve/add", baiduCreateRequest, &result)
if err == nil {
util.Log("新增域名解析 %s 成功! IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
} else {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, err)
domain.UpdateStatus = config.UpdatedFailed
}
}
// modify 更新解析
func (baidu *BaiduCloud) modify(record BaiduRecord, domain *config.Domain, rdType string, ipAddr string) {
//没有变化直接跳过
if record.Rdata == ipAddr {
util.Log("你的IP %s 没有变化, 域名 %s", ipAddr, domain)
return
}
var baiduModifyRequest = BaiduModifyRequest{
RecordId: record.RecordId,
Domain: record.Domain,
View: record.View,
RdType: rdType,
TTL: record.TTL,
Rdata: ipAddr,
ZoneName: record.ZoneName,
}
var result BaiduRecordsResp
err := baidu.request("POST", baiduEndpoint+"/v1/domain/resolve/edit", baiduModifyRequest, &result)
if err == nil {
util.Log("更新域名解析 %s 成功! IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
} else {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err)
domain.UpdateStatus = config.UpdatedFailed
}
}
// request 统一请求接口
func (baidu *BaiduCloud) request(method string, url string, data interface{}, result interface{}) (err error) {
jsonStr := make([]byte, 0)
if data != nil {
jsonStr, _ = json.Marshal(data)
}
req, err := http.NewRequest(
method,
url,
bytes.NewBuffer(jsonStr),
)
if err != nil {
return
}
util.BaiduSigner(baidu.DNS.ID, baidu.DNS.Secret, req)
client := baidu.httpClient
resp, err := client.Do(req)
err = util.GetHTTPResponse(resp, err, result)
return
}
================================================
FILE: dns/callback.go
================================================
package dns
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/jeessy2/ddns-go/v6/config"
"github.com/jeessy2/ddns-go/v6/util"
)
type Callback struct {
DNS config.DNS
Domains config.Domains
TTL string
lastIpv4 string
lastIpv6 string
httpClient *http.Client
}
// Init 初始化
func (cb *Callback) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpCache, ipv6cache *util.IpCache) {
cb.Domains.Ipv4Cache = ipv4cache
cb.Domains.Ipv6Cache = ipv6cache
cb.lastIpv4 = ipv4cache.Addr
cb.lastIpv6 = ipv6cache.Addr
cb.DNS = dnsConf.DNS
cb.Domains.GetNewIp(dnsConf)
if dnsConf.TTL == "" {
// 默认600
cb.TTL = "600"
} else {
cb.TTL = dnsConf.TTL
}
cb.httpClient = dnsConf.GetHTTPClient()
}
// AddUpdateDomainRecords 添加或更新IPv4/IPv6记录
func (cb *Callback) AddUpdateDomainRecords() config.Domains {
cb.addUpdateDomainRecords("A")
cb.addUpdateDomainRecords("AAAA")
return cb.Domains
}
func (cb *Callback) addUpdateDomainRecords(recordType string) {
ipAddr, domains := cb.Domains.GetNewIpResult(recordType)
if ipAddr == "" {
return
}
// 防止多次发送Webhook通知
if recordType == "A" {
if cb.lastIpv4 == ipAddr {
util.Log("你的IPv4未变化, 未触发 %s 请求", "Callback")
return
}
} else {
if cb.lastIpv6 == ipAddr {
util.Log("你的IPv6未变化, 未触发 %s 请求", "Callback")
return
}
}
for _, domain := range domains {
method := "GET"
postPara := ""
contentType := "application/x-www-form-urlencoded"
if cb.DNS.Secret != "" {
method = "POST"
postPara = replacePara(cb.DNS.Secret, ipAddr, domain, recordType, cb.TTL)
if json.Valid([]byte(postPara)) {
contentType = "application/json"
}
}
requestURL := replacePara(cb.DNS.ID, ipAddr, domain, recordType, cb.TTL)
u, err := url.Parse(requestURL)
if err != nil {
util.Log("Callback的URL不正确")
return
}
req, err := http.NewRequest(method, u.String(), strings.NewReader(postPara))
if err != nil {
util.Log("异常信息: %s", err)
domain.UpdateStatus = config.UpdatedFailed
return
}
req.Header.Add("content-type", contentType)
clt := util.CreateHTTPClient()
resp, err := clt.Do(req)
body, err := util.GetHTTPResponseOrg(resp, err)
if err == nil {
util.Log("Callback调用成功, 域名: %s, IP: %s, 返回数据: %s", domain, ipAddr, string(body))
domain.UpdateStatus = config.UpdatedSuccess
} else {
util.Log("Callback调用失败, 异常信息: %s", err)
domain.UpdateStatus = config.UpdatedFailed
}
}
}
// replacePara 替换参数
func replacePara(orgPara, ipAddr string, domain *config.Domain, recordType string, ttl string) string {
// params 使用 map 以便添加更多参数
params := map[string]string{
"ip": ipAddr,
"domain": domain.String(),
"recordType": recordType,
"ttl": ttl,
}
// 也替换域名的自定义参数
for k, v := range domain.GetCustomParams() {
if len(v) == 1 {
params[k] = v[0]
}
}
// 将 map 转换为 [NewReplacer] 所需的参数
// map 中的每个元素占用 2 个位置(kv),因此需要预留 2 倍的空间
oldnew := make([]string, 0, len(params)*2)
for k, v := range params {
k = fmt.Sprintf("#{%s}", k)
oldnew = append(oldnew, k, v)
}
return strings.NewReplacer(oldnew...).Replace(orgPara)
}
================================================
FILE: dns/cloudflare.go
================================================
package dns
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/jeessy2/ddns-go/v6/config"
"github.com/jeessy2/ddns-go/v6/util"
)
const zonesAPI = "https://api.cloudflare.com/client/v4/zones"
// Cloudflare Cloudflare实现
type Cloudflare struct {
DNS config.DNS
Domains config.Domains
TTL int
httpClient *http.Client
}
// CloudflareZonesResp cloudflare zones返回结果
type CloudflareZonesResp struct {
CloudflareStatus
Result []struct {
ID string
Name string
Status string
Paused bool
}
}
// CloudflareRecordsResp records
type CloudflareRecordsResp struct {
CloudflareStatus
Result []CloudflareRecord
}
// CloudflareRecord 记录实体
type CloudflareRecord struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Content string `json:"content"`
Proxied bool `json:"proxied"`
TTL int `json:"ttl"`
Comment string `json:"comment"`
}
// CloudflareStatus 公共状态
type CloudflareStatus struct {
Success bool
Messages []string
}
// Init 初始化
func (cf *Cloudflare) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpCache, ipv6cache *util.IpCache) {
cf.Domains.Ipv4Cache = ipv4cache
cf.Domains.Ipv6Cache = ipv6cache
cf.DNS = dnsConf.DNS
cf.Domains.GetNewIp(dnsConf)
if dnsConf.TTL == "" {
// 默认1 auto ttl
cf.TTL = 1
} else {
ttl, err := strconv.Atoi(dnsConf.TTL)
if err != nil {
cf.TTL = 1
} else {
cf.TTL = ttl
}
}
cf.httpClient = dnsConf.GetHTTPClient()
}
// AddUpdateDomainRecords 添加或更新IPv4/IPv6记录
func (cf *Cloudflare) AddUpdateDomainRecords() config.Domains {
cf.addUpdateDomainRecords("A")
cf.addUpdateDomainRecords("AAAA")
return cf.Domains
}
func (cf *Cloudflare) addUpdateDomainRecords(recordType string) {
ipAddr, domains := cf.Domains.GetNewIpResult(recordType)
if ipAddr == "" {
return
}
for _, domain := range domains {
// get zone
result, err := cf.getZones(domain)
if err != nil {
util.Log("查询域名信息发生异常! %s", err)
domain.UpdateStatus = config.UpdatedFailed
return
}
if len(result.Result) == 0 {
util.Log("在DNS服务商中未找到根域名: %s", domain.DomainName)
domain.UpdateStatus = config.UpdatedFailed
return
}
params := url.Values{}
params.Set("type", recordType)
// The name of DNS records in Cloudflare API expects Punycode.
//
// See: cloudflare/cloudflare-go#690
params.Set("name", domain.ToASCII())
params.Set("per_page", "50")
// Add a comment only if it exists
if c := domain.GetCustomParams().Get("comment"); c != "" {
params.Set("comment", c)
}
zoneID := result.Result[0].ID
var records CloudflareRecordsResp
// getDomains 最多更新前50条
err = cf.request(
"GET",
fmt.Sprintf(zonesAPI+"/%s/dns_records?%s", zoneID, params.Encode()),
nil,
&records,
)
if err != nil {
util.Log("查询域名信息发生异常! %s", err)
domain.UpdateStatus = config.UpdatedFailed
return
}
if !records.Success {
util.Log("查询域名信息发生异常! %s", strings.Join(records.Messages, ", "))
domain.UpdateStatus = config.UpdatedFailed
return
}
if len(records.Result) > 0 {
// 更新
cf.modify(records, zoneID, domain, ipAddr)
} else {
// 新增
cf.create(zoneID, domain, recordType, ipAddr)
}
}
}
// 创建
func (cf *Cloudflare) create(zoneID string, domain *config.Domain, recordType string, ipAddr string) {
record := &CloudflareRecord{
Type: recordType,
Name: domain.ToASCII(),
Content: ipAddr,
Proxied: false,
TTL: cf.TTL,
Comment: domain.GetCustomParams().Get("comment"),
}
record.Proxied = domain.GetCustomParams().Get("proxied") == "true"
var status CloudflareStatus
err := cf.request(
"POST",
fmt.Sprintf(zonesAPI+"/%s/dns_records", zoneID),
record,
&status,
)
if err != nil {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, err)
domain.UpdateStatus = config.UpdatedFailed
return
}
if status.Success {
util.Log("新增域名解析 %s 成功! IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
} else {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, strings.Join(status.Messages, ", "))
domain.UpdateStatus = config.UpdatedFailed
}
}
// 修改
func (cf *Cloudflare) modify(result CloudflareRecordsResp, zoneID string, domain *config.Domain, ipAddr string) {
for _, record := range result.Result {
// 相同不修改
if record.Content == ipAddr {
util.Log("你的IP %s 没有变化, 域名 %s", ipAddr, domain)
continue
}
var status CloudflareStatus
record.Content = ipAddr
record.TTL = cf.TTL
// 存在参数才修改proxied
if domain.GetCustomParams().Has("proxied") {
record.Proxied = domain.GetCustomParams().Get("proxied") == "true"
}
err := cf.request(
"PUT",
fmt.Sprintf(zonesAPI+"/%s/dns_records/%s", zoneID, record.ID),
record,
&status,
)
if err != nil {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err)
domain.UpdateStatus = config.UpdatedFailed
return
}
if status.Success {
util.Log("更新域名解析 %s 成功! IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
} else {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, strings.Join(status.Messages, ", "))
domain.UpdateStatus = config.UpdatedFailed
}
}
}
// 获得域名记录列表
func (cf *Cloudflare) getZones(domain *config.Domain) (result CloudflareZonesResp, err error) {
params := url.Values{}
params.Set("name", domain.DomainName)
params.Set("status", "active")
params.Set("per_page", "50")
err = cf.request(
"GET",
fmt.Sprintf(zonesAPI+"?%s", params.Encode()),
nil,
&result,
)
return
}
// request 统一请求接口
func (cf *Cloudflare) request(method string, url string, data interface{}, result interface{}) (err error) {
jsonStr := make([]byte, 0)
if data != nil {
jsonStr, _ = json.Marshal(data)
}
req, err := http.NewRequest(
method,
url,
bytes.NewBuffer(jsonStr),
)
if err != nil {
return
}
req.Header.Set("Authorization", "Bearer "+cf.DNS.Secret)
req.Header.Set("Content-Type", "application/json")
client := cf.httpClient
resp, err := client.Do(req)
err = util.GetHTTPResponse(resp, err, result)
return
}
================================================
FILE: dns/dnsla.go
================================================
package dns
import (
"bytes"
"encoding/base64"
"encoding/json"
"github.com/jeessy2/ddns-go/v6/config"
"github.com/jeessy2/ddns-go/v6/util"
"io"
"net/http"
"strconv"
)
const (
recordList string = "http://api.dns.la/api/recordList"
recordModify string = "http://api.dns.la/api/record"
recordCreate string = "http://api.dns.la/api/record"
)
// https://www.dns.la/docs/ApiDoc
// dnsla dnsla实现
type Dnsla struct {
DNS config.DNS
Domains config.Domains
TTL int
httpClient *http.Client
}
// DnslaRecord
type DnslaRecord struct {
ID string `json:"id"`
Host string `json:"host"`
Type int `json:"type"`
Data string `json:"data"`
}
// DnslaRecordListResp recordListAPI结果
type DnslaRecordListResp struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data struct {
Total int `json:"total"`
Results []DnslaRecord `json:"results"`
} `json:"data"`
}
// DnslaStatus DnslaStatus
type DnslaStatus struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data struct {
Id string `json:"id"`
} `json:"data"`
}
// Init 初始化
func (dnsla *Dnsla) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpCache, ipv6cache *util.IpCache) {
dnsla.Domains.Ipv4Cache = ipv4cache
dnsla.Domains.Ipv6Cache = ipv6cache
dnsla.DNS = dnsConf.DNS
dnsla.Domains.GetNewIp(dnsConf)
if dnsConf.TTL == "" {
// 默认600s
dnsla.TTL = 600
} else {
ttlInt, _ := strconv.Atoi(dnsConf.TTL)
dnsla.TTL = ttlInt
}
dnsla.httpClient = dnsConf.GetHTTPClient()
}
// AddUpdateDomainRecords 添加或更新IPv4/IPv6记录
func (dnsla *Dnsla) AddUpdateDomainRecords() config.Domains {
dnsla.addUpdateDomainRecords("A")
dnsla.addUpdateDomainRecords("AAAA")
return dnsla.Domains
}
func (dnsla *Dnsla) addUpdateDomainRecords(recordType string) {
ipAddr, domains := dnsla.Domains.GetNewIpResult(recordType)
if ipAddr == "" {
return
}
for _, domain := range domains {
resultByte, err := dnsla.getRecordList(domain, recordType)
if err != nil {
util.Log("查询域名信息发生异常! %s", err)
domain.UpdateStatus = config.UpdatedFailed
return
}
var jsonResult DnslaRecordListResp
errU := json.Unmarshal(resultByte, &jsonResult)
if errU != nil {
util.Log(errU.Error())
return
}
if jsonResult.Data.Total > 0 { // 默认第一个
recordSelected := jsonResult.Data.Results[0]
params := domain.GetCustomParams()
if params.Has("id") {
for i := 0; i < len(jsonResult.Data.Results); i++ {
if jsonResult.Data.Results[i].ID == params.Get("id") {
recordSelected = jsonResult.Data.Results[i]
}
}
}
// 更新
dnsla.modify(recordSelected, domain, recordType, ipAddr)
} else {
// 新增
dnsla.create(domain, recordType, ipAddr)
}
}
}
// 创建
func (dnsla *Dnsla) create(domain *config.Domain, recordType string, ipAddr string) {
recordTypeInt := 1
if recordType == "AAAA" {
recordTypeInt = 28
}
type CreateParams struct {
Domain string `json:"Domain"`
Host string `json:"Host"`
Type int `json:"Type"`
Data string `json:"Data"`
TTL int `json:"TTL"`
}
createParams := CreateParams{
Domain: domain.DomainName,
Host: domain.GetSubDomain(),
Type: recordTypeInt,
Data: ipAddr,
TTL: dnsla.TTL,
}
jsonData, _ := json.Marshal(createParams)
resultByte, err := dnsla.request("POST", recordCreate, jsonData)
if err != nil {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, err)
domain.UpdateStatus = config.UpdatedFailed
return
}
var jsonResult DnslaStatus
errU := json.Unmarshal(resultByte, &jsonResult)
if errU != nil {
util.Log(errU.Error())
return
}
if jsonResult.Code == 200 {
util.Log("新增域名解析 %s 成功! IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
} else {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, jsonResult.Msg)
domain.UpdateStatus = config.UpdatedFailed
}
}
// 修改
func (dnsla *Dnsla) modify(record DnslaRecord, domain *config.Domain, recordType string, ipAddr string) {
// 相同不修改
if record.Data == ipAddr {
util.Log("你的IP %s 没有变化, 域名 %s", ipAddr, domain)
return
}
recordTypeInt := 1
if recordType == "AAAA" {
recordTypeInt = 28
}
type ModifyParams struct {
ID string `json:"Id"`
Host string `json:"Host"`
Type int `json:"Type"`
Data string `json:"Data"`
TTL int `json:"TTL"`
}
modifyParams := ModifyParams{
ID: record.ID,
Host: domain.GetSubDomain(),
Type: recordTypeInt,
Data: ipAddr,
TTL: dnsla.TTL,
}
jsonData, _ := json.Marshal(modifyParams)
resultByte, err := dnsla.request("PUT", recordModify, jsonData)
if err != nil {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err)
domain.UpdateStatus = config.UpdatedFailed
return
}
var jsonResult DnslaStatus
errU := json.Unmarshal(resultByte, &jsonResult)
if errU != nil {
util.Log(errU.Error())
return
}
if jsonResult.Code == 200 {
util.Log("更新域名解析 %s 成功! IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
} else {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, jsonResult.Msg)
domain.UpdateStatus = config.UpdatedFailed
}
}
// request sends a POST request to the given API with the given values.
func (dnsla *Dnsla) request(method, apiAddr string, values []byte) (body []byte, err error) {
req, err := http.NewRequest(
method,
apiAddr,
bytes.NewReader(values),
)
if err != nil {
panic(err)
}
// 设置自定义 Headers
byteBuff := []byte(dnsla.DNS.ID + ":" + dnsla.DNS.Secret)
token := "Basic " + base64.StdEncoding.EncodeToString(byteBuff)
req.Header.Set("Authorization", token)
req.Header.Set("Content-Type", "application/json;charset=utf-8")
// 4. 发送请求
client := dnsla.httpClient
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ = io.ReadAll(resp.Body)
return
}
// 获得域名记录列表
func (dnsla *Dnsla) getRecordList(domain *config.Domain, typ string) (result []byte, err error) {
recordTypeInt := "1"
if typ == "AAAA" {
recordTypeInt = "28"
}
params := domain.GetCustomParams()
params.Set("domain", domain.DomainName)
params.Set("host", domain.GetSubDomain())
params.Set("type", recordTypeInt)
params.Set("pageIndex", "1")
params.Set("pageSize", "999")
url := recordList + "?" + params.Encode()
req, err := http.NewRequest("GET", url, nil)
if err != nil {
panic(err)
}
byteBuff := []byte(dnsla.DNS.ID + ":" + dnsla.DNS.Secret)
token := "Basic " + base64.StdEncoding.EncodeToString(byteBuff)
// 设置 Headers
req.Header.Set("Authorization", token)
// 发送请求
client := dnsla.httpClient
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
// 读取响应
result, errR := io.ReadAll(resp.Body)
if errR != nil {
util.Log(errR.Error())
return
}
return
}
================================================
FILE: dns/dnspod.go
================================================
package dns
import (
"net/http"
"net/url"
"github.com/jeessy2/ddns-go/v6/config"
"github.com/jeessy2/ddns-go/v6/util"
)
const (
recordListAPI string = "https://dnsapi.cn/Record.List"
recordModifyURL string = "https://dnsapi.cn/Record.Modify"
recordCreateAPI string = "https://dnsapi.cn/Record.Create"
)
// https://cloud.tencent.com/document/api/302/8516
// Dnspod 腾讯云dns实现
type Dnspod struct {
DNS config.DNS
Domains config.Domains
TTL string
httpClient *http.Client
}
// DnspodRecord DnspodRecord
type DnspodRecord struct {
ID string
Name string
Type string
Value string
Enabled string
}
// DnspodRecordListResp recordListAPI结果
type DnspodRecordListResp struct {
DnspodStatus
Records []DnspodRecord
}
// DnspodStatus DnspodStatus
type DnspodStatus struct {
Status struct {
Code string
Message string
}
}
// Init 初始化
func (dnspod *Dnspod) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpCache, ipv6cache *util.IpCache) {
dnspod.Domains.Ipv4Cache = ipv4cache
dnspod.Domains.Ipv6Cache = ipv6cache
dnspod.DNS = dnsConf.DNS
dnspod.Domains.GetNewIp(dnsConf)
if dnsConf.TTL == "" {
// 默认600s
dnspod.TTL = "600"
} else {
dnspod.TTL = dnsConf.TTL
}
dnspod.httpClient = dnsConf.GetHTTPClient()
}
// AddUpdateDomainRecords 添加或更新IPv4/IPv6记录
func (dnspod *Dnspod) AddUpdateDomainRecords() config.Domains {
dnspod.addUpdateDomainRecords("A")
dnspod.addUpdateDomainRecords("AAAA")
return dnspod.Domains
}
func (dnspod *Dnspod) addUpdateDomainRecords(recordType string) {
ipAddr, domains := dnspod.Domains.GetNewIpResult(recordType)
if ipAddr == "" {
return
}
for _, domain := range domains {
result, err := dnspod.getRecordList(domain, recordType)
if err != nil {
util.Log("查询域名信息发生异常! %s", err)
domain.UpdateStatus = config.UpdatedFailed
return
}
if len(result.Records) > 0 {
// 默认第一个
recordSelected := result.Records[0]
params := domain.GetCustomParams()
if params.Has("record_id") {
for i := 0; i < len(result.Records); i++ {
if result.Records[i].ID == params.Get("record_id") {
recordSelected = result.Records[i]
}
}
}
// 更新
dnspod.modify(recordSelected, domain, recordType, ipAddr)
} else {
// 新增
dnspod.create(domain, recordType, ipAddr)
}
}
}
// 创建
func (dnspod *Dnspod) create(domain *config.Domain, recordType string, ipAddr string) {
params := domain.GetCustomParams()
params.Set("login_token", dnspod.DNS.ID+","+dnspod.DNS.Secret)
params.Set("domain", domain.DomainName)
params.Set("sub_domain", domain.GetSubDomain())
params.Set("record_type", recordType)
params.Set("value", ipAddr)
params.Set("ttl", dnspod.TTL)
params.Set("format", "json")
if !params.Has("record_line") {
params.Set("record_line", "默认")
}
status, err := dnspod.request(recordCreateAPI, params)
if err != nil {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, err)
domain.UpdateStatus = config.UpdatedFailed
return
}
if status.Status.Code == "1" {
util.Log("新增域名解析 %s 成功! IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
} else {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, status.Status.Message)
domain.UpdateStatus = config.UpdatedFailed
}
}
// 修改
func (dnspod *Dnspod) modify(record DnspodRecord, domain *config.Domain, recordType string, ipAddr string) {
// 相同不修改
if record.Value == ipAddr {
util.Log("你的IP %s 没有变化, 域名 %s", ipAddr, domain)
return
}
params := domain.GetCustomParams()
params.Set("login_token", dnspod.DNS.ID+","+dnspod.DNS.Secret)
params.Set("domain", domain.DomainName)
params.Set("sub_domain", domain.GetSubDomain())
params.Set("record_type", recordType)
params.Set("value", ipAddr)
params.Set("ttl", dnspod.TTL)
params.Set("format", "json")
params.Set("record_id", record.ID)
if !params.Has("record_line") {
params.Set("record_line", "默认")
}
status, err := dnspod.request(recordModifyURL, params)
if err != nil {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err)
domain.UpdateStatus = config.UpdatedFailed
return
}
if status.Status.Code == "1" {
util.Log("更新域名解析 %s 成功! IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
} else {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, status.Status.Message)
domain.UpdateStatus = config.UpdatedFailed
}
}
// request sends a POST request to the given API with the given values.
func (dnspod *Dnspod) request(apiAddr string, values url.Values) (status DnspodStatus, err error) {
client := dnspod.httpClient
resp, err := client.PostForm(
apiAddr,
values,
)
err = util.GetHTTPResponse(resp, err, &status)
return
}
// 获得域名记录列表
func (dnspod *Dnspod) getRecordList(domain *config.Domain, typ string) (result DnspodRecordListResp, err error) {
params := domain.GetCustomParams()
params.Set("login_token", dnspod.DNS.ID+","+dnspod.DNS.Secret)
params.Set("domain", domain.DomainName)
params.Set("record_type", typ)
params.Set("sub_domain", domain.GetSubDomain())
params.Set("format", "json")
client := dnspod.httpClient
resp, err := client.PostForm(
recordListAPI,
params,
)
err = util.GetHTTPResponse(resp, err, &result)
return
}
================================================
FILE: dns/dynadot.go
================================================
package dns
import (
"bytes"
"github.com/jeessy2/ddns-go/v6/config"
"github.com/jeessy2/ddns-go/v6/util"
"net/http"
"net/url"
"strconv"
"strings"
)
// https://www.dynadot.com/set_ddns
const (
dynadotEndpoint string = "https://www.dynadot.com/set_ddns"
)
// Dynadot Dynadot
type Dynadot struct {
DNS config.DNS
Domains config.Domains
TTL string
LastIpv4 string
LastIpv6 string
httpClient *http.Client
}
// DynadotRecord record
type DynadotRecord struct {
DomainName string
SubDomainNames []string
CustomParams url.Values
Domains []*config.Domain
ContainRoot bool
}
// DynadotResp 修改/添加返回结果
type DynadotResp struct {
Status string `json:"status"`
ErrorCode int `json:"error_code"`
Content []string `json:"content"`
}
// Init 初始化
func (dynadot *Dynadot) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpCache, ipv6cache *util.IpCache) {
dynadot.Domains.Ipv4Cache = ipv4cache
dynadot.Domains.Ipv6Cache = ipv6cache
dynadot.LastIpv4 = ipv4cache.Addr
dynadot.LastIpv6 = ipv6cache.Addr
dynadot.DNS = dnsConf.DNS
dynadot.Domains.GetNewIp(dnsConf)
if dnsConf.TTL == "" {
// 默认600s
dynadot.TTL = "600"
} else {
dynadot.TTL = dnsConf.TTL
}
dynadot.httpClient = dnsConf.GetHTTPClient()
}
// AddUpdateDomainRecords 添加或更新IPv4/IPv6记录
func (dynadot *Dynadot) AddUpdateDomainRecords() config.Domains {
dynadot.addOrUpdateDomainRecords("A")
dynadot.addOrUpdateDomainRecords("AAAA")
return dynadot.Domains
}
// addOrUpdateDomainRecords 添加或更新记录
func (dynadot *Dynadot) addOrUpdateDomainRecords(recordType string) {
ipAddr, domains := dynadot.Domains.GetNewIpResult(recordType)
if len(ipAddr) == 0 {
return
}
// 防止多次发送Webhook通知
if recordType == "A" {
if dynadot.LastIpv4 == ipAddr {
util.Log("你的IPv4未变化, 未触发 %s 请求", "dynadot")
return
}
} else {
if dynadot.LastIpv6 == ipAddr {
util.Log("你的IPv6未变化, 未触发 %s 请求", "dynadot")
return
}
}
records := mergeDomains(domains)
// dynadot 仅支持一个域名对应一个dynamic password
if len(records) != 1 {
util.Log("dynadot仅支持单域名配置,多个域名请添加更多配置")
return
}
for _, record := range records {
// 创建或更新
dynadot.createOrModify(record, recordType, ipAddr)
}
}
// 合并域名的子域名
func mergeDomains(domains []*config.Domain) (records []*DynadotRecord) {
records = make([]*DynadotRecord, 0)
for _, domain := range domains {
var record *DynadotRecord
for _, r := range records {
if r.DomainName == domain.DomainName {
record = r
params := domain.GetCustomParams()
for key := range params {
record.CustomParams.Add(key, params.Get(key))
}
record.Domains = append(record.Domains, domain)
record.SubDomainNames = append(record.SubDomainNames, domain.GetSubDomain())
break
}
}
if record == nil {
record = &DynadotRecord{
DomainName: domain.DomainName,
CustomParams: domain.GetCustomParams(),
Domains: []*config.Domain{domain},
SubDomainNames: []string{domain.GetSubDomain()},
}
records = append(records, record)
}
if len(domain.SubDomain) == 0 {
// 包含根域名
record.ContainRoot = true
}
}
return records
}
// 创建或变更记录
func (dynadot *Dynadot) createOrModify(record *DynadotRecord, recordType string, ipAddr string) {
params := record.CustomParams
params.Set("domain", record.DomainName)
params.Set("subDomain", strings.Join(record.SubDomainNames, ","))
params.Set("type", recordType)
params.Set("ip", ipAddr)
params.Set("pwd", dynadot.DNS.Secret)
params.Set("ttl", dynadot.TTL)
params.Set("containRoot", strconv.FormatBool(record.ContainRoot))
var result DynadotResp
err := dynadot.request(params, &result)
domains := record.Domains
for _, domain := range domains {
if err != nil {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err)
domain.UpdateStatus = config.UpdatedFailed
return
}
if result.ErrorCode != -1 {
util.Log("更新域名解析 %s 成功! IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
} else {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, strings.Join(result.Content, ","))
domain.UpdateStatus = config.UpdatedFailed
}
}
}
// request 统一请求接口
func (dynadot *Dynadot) request(params url.Values, result interface{}) (err error) {
req, err := http.NewRequest(
"GET",
dynadotEndpoint,
bytes.NewBuffer(nil),
)
req.URL.RawQuery = params.Encode()
if err != nil {
return
}
client := dynadot.httpClient
resp, err := client.Do(req)
err = util.GetHTTPResponse(resp, err, result)
return
}
================================================
FILE: dns/dynv6.go
================================================
package dns
import (
"bytes"
"encoding/json"
"github.com/jeessy2/ddns-go/v6/config"
"github.com/jeessy2/ddns-go/v6/util"
"net/http"
"strconv"
"strings"
)
const (
dynv6Endpoint = "https://dynv6.com"
)
type Dynv6 struct {
DNS config.DNS
Domains config.Domains
TTL string
httpClient *http.Client
}
type Dynv6Zone struct {
ID uint `json:"id"`
Name string `json:"name"`
Ipv4 string `json:"ipv4address"`
Ipv6 string `json:"ipv6prefix"`
}
type Dynv6Record struct {
ID uint `json:"id"`
ZoneID uint `json:"zoneID"`
Name string `json:"name"`
Type string `json:"type"`
Data string `json:"data"`
}
// Init 初始化
func (dynv6 *Dynv6) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpCache, ipv6cache *util.IpCache) {
dynv6.Domains.Ipv4Cache = ipv4cache
dynv6.Domains.Ipv6Cache = ipv6cache
dynv6.DNS = dnsConf.DNS
dynv6.Domains.GetNewIp(dnsConf)
if dnsConf.TTL == "" {
// 默认600s
dynv6.TTL = "600"
} else {
dynv6.TTL = dnsConf.TTL
}
dynv6.httpClient = dnsConf.GetHTTPClient()
}
// AddUpdateDomainRecords 添加或更新IPv4/IPv6记录
func (dynv6 *Dynv6) AddUpdateDomainRecords() config.Domains {
dynv6.addUpdateDomainRecords("A")
dynv6.addUpdateDomainRecords("AAAA")
return dynv6.Domains
}
func (dynv6 *Dynv6) addUpdateDomainRecords(recordType string) {
ipAddr, domains := dynv6.Domains.GetNewIpResult(recordType)
if ipAddr == "" {
return
}
for _, domain := range domains {
isFindZone, findZone, isMain, err := dynv6.findZone(domain)
if err != nil {
util.Log("查询域名信息发生异常! %s", err)
domain.UpdateStatus = config.UpdatedFailed
return
}
if !isFindZone {
util.Log("在DNS服务商中未找到根域名: %s", domain)
domain.UpdateStatus = config.UpdatedFailed
continue
}
zoneId := strconv.FormatUint(uint64(findZone.ID), 10)
if isMain {
// 如果使用的域名是主域名,对比DNS记录确定是否调用更新接口
if (recordType == "A" && findZone.Ipv4 == ipAddr) || (recordType == "AAAA" && findZone.Ipv6 == ipAddr) {
// ip与dns服务器一致,不执行更新
util.Log("你的IP %s 没有变化, 域名 %s", ipAddr, domain)
domain.UpdateStatus = config.UpdatedNothing
} else {
dynv6.modifyMain(domain, zoneId, recordType, ipAddr)
}
} else {
// 如果是子域名,检查是否有该子域名记录,有就更新记录,没有就创建
// 处理subDomain
processSubDomainOk := dynv6.processSubDomain(domain, findZone)
if !processSubDomainOk {
util.Log("域名: %s 不正确", domain)
domain.UpdateStatus = config.UpdatedFailed
continue
}
isFindRecord, findRecord, err := dynv6.findRecord(domain, zoneId, recordType)
if err != nil {
util.Log("查询域名信息发生异常! %s", err)
domain.UpdateStatus = config.UpdatedFailed
return
}
if isFindRecord {
// 判断是否需要更新
if findRecord.Type == recordType && findRecord.Data == ipAddr {
// ip与dns服务器一致,不执行更新
util.Log("你的IP %s 没有变化, 域名 %s", ipAddr, domain)
domain.UpdateStatus = config.UpdatedNothing
} else {
dynv6.modify(domain, zoneId, findRecord, recordType, ipAddr)
}
} else {
// 创建记录
dynv6.create(domain, zoneId, recordType, ipAddr)
}
}
}
}
func (dynv6 *Dynv6) processSubDomain(domain *config.Domain, zone Dynv6Zone) bool {
// 确定subDomain
subDomainLen := len(domain.String()) - len(zone.Name) - 1
if subDomainLen <= 0 {
return false
}
subDomain := domain.String()[:subDomainLen]
domain.DomainName = zone.Name
domain.SubDomain = subDomain
return true
}
// 根据domain获取zone
func (dynv6 *Dynv6) findZone(domain *config.Domain) (isFind bool, zone Dynv6Zone, isMain bool, err error) {
var zones []Dynv6Zone
isFind = false
isMain = false
// 获取所有zone
err = dynv6.request("GET", dynv6Endpoint+"/api/v2/zones", nil, &zones)
if err != nil {
return
}
// 遍历token权限下所有zone,确定当前域名属于哪个zone,并判断当前域名是主域名还是子域名
for _, z := range zones {
if strings.HasSuffix(domain.String(), z.Name) {
isFind = true
zone = z
if domain.String() == z.Name {
isMain = true
}
break
}
}
return
}
// 根据domain获取record
func (dynv6 *Dynv6) findRecord(domain *config.Domain, zoneId string, recordType string) (isFind bool, record Dynv6Record, err error) {
var records []Dynv6Record
isFind = false
err = dynv6.request("GET", dynv6Endpoint+"/api/v2/zones/"+zoneId+"/records", nil, &records)
if err != nil {
return
}
// 遍历zone下所有record,判断是更新还是创建
for _, r := range records {
if r.Name == domain.SubDomain && r.Type == recordType {
isFind = true
record = r
break
}
}
return
}
// modify 更新根域名
func (dynv6 *Dynv6) modifyMain(domain *config.Domain, zoneId string, recordType string, ipAddr string) {
var zoneUpdateReq = Dynv6Zone{}
if recordType == "A" {
zoneUpdateReq.Ipv4 = ipAddr
} else {
zoneUpdateReq.Ipv6 = ipAddr
}
err := dynv6.request("PATCH", dynv6Endpoint+"/api/v2/zones/"+zoneId, zoneUpdateReq, &Dynv6Zone{})
if err != nil {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err)
domain.UpdateStatus = config.UpdatedFailed
} else {
util.Log("更新域名解析 %s 成功! IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
}
}
// create 创建新的解析
func (dynv6 *Dynv6) create(domain *config.Domain, zoneId string, recordType string, ipAddr string) {
recordUpdateReq := Dynv6Record{
Name: domain.SubDomain,
Type: recordType,
Data: ipAddr,
}
err := dynv6.request("POST", dynv6Endpoint+"/api/v2/zones/"+zoneId+"/records", recordUpdateReq, &Dynv6Record{})
if err != nil {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, err)
domain.UpdateStatus = config.UpdatedFailed
} else {
util.Log("新增域名解析 %s 成功! IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
}
}
// modify 更新解析
func (dynv6 *Dynv6) modify(domain *config.Domain, zoneId string, record Dynv6Record, recordType string, ipAddr string) {
record.Type = recordType
record.Data = ipAddr
recordId := strconv.FormatUint(uint64(record.ID), 10)
err := dynv6.request("PATCH", dynv6Endpoint+"/api/v2/zones/"+zoneId+"/records/"+recordId, record, &Dynv6Record{})
if err != nil {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err)
domain.UpdateStatus = config.UpdatedFailed
} else {
util.Log("更新域名解析 %s 成功! IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
}
}
// request 统一请求接口
func (dynv6 *Dynv6) request(method string, url string, data interface{}, result interface{}) (err error) {
jsonStr := make([]byte, 0)
if data != nil {
jsonStr, _ = json.Marshal(data)
}
req, err := http.NewRequest(
method,
url,
bytes.NewBuffer(jsonStr),
)
if err != nil {
return err
}
req.Header.Add("Authorization", "Bearer "+dynv6.DNS.Secret)
req.Header.Set("Content-Type", "application/json")
client := dynv6.httpClient
resp, err := client.Do(req)
err = util.GetHTTPResponse(resp, err, result)
return err
}
================================================
FILE: dns/edgeone.go
================================================
package dns
import (
"bytes"
"encoding/json"
"net/http"
"strconv"
"github.com/jeessy2/ddns-go/v6/config"
"github.com/jeessy2/ddns-go/v6/util"
"golang.org/x/net/idna"
)
// https://cloud.tencent.com/document/api/1552/80730
const (
edgeoneEndPoint = "https://teo.tencentcloudapi.com"
edgeoneVersion = "2022-09-01"
)
type EdgeOne struct {
DNS config.DNS
Domains config.Domains
TTL int
httpClient *http.Client
}
type EdgeOneRecord struct {
ZoneId string `json:"ZoneId"`
Name string `json:"Name"` // FullDomain
Type string `json:"Type"` // record type, e.g. A AAAA
Content string `json:"Content"`
Location string `json:"Location"`
TTL int `json:"TTL"`
Weight int `json:"Weight,omitempty"`
RecordId string `json:"RecordId,omitempty"`
Status string `json:"Status,omitempty"`
}
type EdgeOneRecordResponse struct {
EdgeOneStatus
Response struct {
DnsRecords []EdgeOneRecord `json:"DnsRecords"`
TotalCount int `json:"TotalCount"`
}
}
type EdgeOneZoneResponse struct {
EdgeOneStatus
Response struct {
TotalCount int `json:"TotalCount"`
Zones []struct {
ZoneId string `json:"ZoneId"`
ZoneName string `json:"ZoneName"`
} `json:"Zones"`
}
}
type Filter struct {
Name string `json:"Name"`
Values []string `json:"Values"`
}
type EdgeOneDescribeDns struct {
ZoneId string `json:"ZoneId,omitempty"`
Filters []Filter `json:"Filters"`
}
// https://cloud.tencent.com/document/product/1552/80729
type EdgeOneStatus struct {
Response struct {
Error struct {
Code string
Message string
}
}
}
// Init 初始化
func (eo *EdgeOne) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpCache, ipv6cache *util.IpCache) {
eo.Domains.Ipv4Cache = ipv4cache
eo.Domains.Ipv6Cache = ipv6cache
eo.DNS = dnsConf.DNS
eo.Domains.GetNewIp(dnsConf)
if dnsConf.TTL == "" {
// 默认 600s
eo.TTL = 600
} else {
ttl, err := strconv.Atoi(dnsConf.TTL)
if err != nil {
eo.TTL = 600
} else {
eo.TTL = ttl
}
}
eo.httpClient = dnsConf.GetHTTPClient()
}
// AddUpdateDomainRecords 添加或更新 IPv4/IPv6 记录
func (eo *EdgeOne) AddUpdateDomainRecords() config.Domains {
eo.addUpdateDomainRecords("A")
eo.addUpdateDomainRecords("AAAA")
return eo.Domains
}
func (eo *EdgeOne) addUpdateDomainRecords(recordType string) {
ipAddr, domains := eo.Domains.GetNewIpResult(recordType)
if ipAddr == "" {
return
}
for _, domain := range domains {
zoneResult, err := eo.getZone(domain.DomainName)
if err != nil || zoneResult.Response.TotalCount <= 0 || zoneResult.Response.Zones[0].ZoneName != domain.DomainName {
util.Log("查询域名信息发生异常! %s", err)
domain.UpdateStatus = config.UpdatedFailed
return
}
zoneId := zoneResult.Response.Zones[0].ZoneId
recordResult, err := eo.getRecordList(domain, recordType, zoneId)
if err != nil {
util.Log("查询域名信息发生异常! %s", err)
domain.UpdateStatus = config.UpdatedFailed
return
}
params := domain.GetCustomParams()
var isValid func(*EdgeOneRecord) bool
if params.Has("RecordId") {
isValid = func(r *EdgeOneRecord) bool { return r.RecordId == params.Get("RecordId") }
} else {
isValid = func(r *EdgeOneRecord) bool {
return r.Status == "enable" || r.Status == "disable" && r.Content == ipAddr
}
}
var recordSelected *EdgeOneRecord
for i := range recordResult.Response.DnsRecords {
r := &recordResult.Response.DnsRecords[i]
if isValid(r) {
recordSelected = r
break
}
}
if recordSelected != nil {
// 修改记录
eo.modify(*recordSelected, domain, recordType, ipAddr, zoneId)
} else {
// 添加记录
eo.create(domain, recordType, ipAddr, zoneId)
}
}
}
// CreateDnsRecord https://cloud.tencent.com/document/product/1552/80720
func (eo *EdgeOne) create(domain *config.Domain, recordType string, ipAddr string, ZoneId string) {
d := domain.DomainName
if domain.SubDomain != "" && domain.SubDomain != "@" {
d = domain.SubDomain + "." + domain.DomainName
}
asciiDomain, _ := idna.ToASCII(d)
record := &EdgeOneRecord{
ZoneId: ZoneId,
Name: asciiDomain,
Type: recordType,
Content: ipAddr,
Location: eo.getLocation(domain),
TTL: eo.TTL,
}
var status EdgeOneStatus
err := eo.request(
"CreateDnsRecord",
record,
&status,
)
if err != nil {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, err)
domain.UpdateStatus = config.UpdatedFailed
return
}
if status.Response.Error.Code == "" {
util.Log("新增域名解析 %s 成功! IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
} else {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, status.Response.Error.Message)
domain.UpdateStatus = config.UpdatedFailed
}
}
// ModifyDnsRecords https://cloud.tencent.com/document/product/1552/114252
func (eo *EdgeOne) modify(record EdgeOneRecord, domain *config.Domain, recordType string, ipAddr string, ZoneId string) {
// 相同不修改
if record.Content == ipAddr {
util.Log("你的IP %s 没有变化, 域名 %s", ipAddr, domain)
return
}
var status EdgeOneStatus
d := domain.DomainName
if domain.SubDomain != "" && domain.SubDomain != "@" {
d = domain.SubDomain + "." + domain.DomainName
}
asciiDomain, _ := idna.ToASCII(d)
record.ZoneId = ZoneId
record.Name = asciiDomain
record.Type = recordType
record.Content = ipAddr
record.Location = eo.getLocation(domain)
record.TTL = eo.TTL
err := eo.request(
"ModifyDnsRecords",
struct {
ZoneId string `json:"ZoneId"`
DnsRecords []EdgeOneRecord `json:"DnsRecords"`
}{
ZoneId: ZoneId,
DnsRecords: []EdgeOneRecord{record},
},
&status,
)
if err != nil {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err)
domain.UpdateStatus = config.UpdatedFailed
return
}
if status.Response.Error.Code == "" {
util.Log("更新域名解析 %s 成功! IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
} else {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, status.Response.Error.Message)
domain.UpdateStatus = config.UpdatedFailed
}
}
func (eo *EdgeOne) getZone(domain string) (result EdgeOneZoneResponse, err error) {
asciiDomain, _ := idna.ToASCII(domain)
record := EdgeOneDescribeDns{
Filters: []Filter{
{Name: "zone-name", Values: []string{asciiDomain}},
},
}
err = eo.request(
"DescribeZones",
record,
&result,
)
return
}
// DescribeDnsRecords https://cloud.tencent.com/document/product/1552/80716
func (eo *EdgeOne) getRecordList(domain *config.Domain, recordType string, ZoneId string) (result EdgeOneRecordResponse, err error) {
d := domain.DomainName
if domain.SubDomain != "" && domain.SubDomain != "@" {
d = domain.SubDomain + "." + domain.DomainName
}
asciiDomain, _ := idna.ToASCII(d)
record := EdgeOneDescribeDns{
ZoneId: ZoneId,
Filters: []Filter{
{Name: "name", Values: []string{asciiDomain}},
{Name: "type", Values: []string{recordType}},
},
}
err = eo.request(
"DescribeDnsRecords",
record,
&result,
)
return
}
// getLocation 获取记录线路,为空返回默认
func (eo *EdgeOne) getLocation(domain *config.Domain) string {
if domain.GetCustomParams().Has("Location") {
return domain.GetCustomParams().Get("Location")
}
return "Default"
}
// request 统一请求接口
func (eo *EdgeOne) request(action string, data interface{}, result interface{}) (err error) {
jsonStr := make([]byte, 0)
if data != nil {
jsonStr, _ = json.Marshal(data)
}
req, err := http.NewRequest(
"POST",
edgeoneEndPoint,
bytes.NewBuffer(jsonStr),
)
if err != nil {
return
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-TC-Version", edgeoneVersion)
util.TencentCloudSigner(eo.DNS.ID, eo.DNS.Secret, req, action, string(jsonStr), util.EdgeOne)
client := eo.httpClient
resp, err := client.Do(req)
err = util.GetHTTPResponse(resp, err, result)
return
}
================================================
FILE: dns/eranet.go
================================================
package dns
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"time"
"github.com/jeessy2/ddns-go/v6/config"
"github.com/jeessy2/ddns-go/v6/util"
)
// Eranet DNS实现
type Eranet struct {
DNS config.DNS
Domains config.Domains
TTL string
httpClient *http.Client
}
type EranetRecord struct {
ID int `json:"id"`
Domain string
Host string
Type string
Value string
State int
// Name string
// Enabled string
}
type EranetRecordListResp struct {
EranetBaseResult
Data []EranetRecord
}
type EranetBaseResult struct {
RequestId string `json:"RequestId"`
Id int `json:"Id"`
Error string `json:"error"`
}
// Init 初始化
func (eranet *Eranet) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpCache, ipv6cache *util.IpCache) {
eranet.Domains.Ipv4Cache = ipv4cache
eranet.Domains.Ipv6Cache = ipv6cache
eranet.DNS = dnsConf.DNS
eranet.Domains.GetNewIp(dnsConf)
if dnsConf.TTL == "" {
// 默认600s
eranet.TTL = "600"
} else {
eranet.TTL = dnsConf.TTL
}
eranet.httpClient = dnsConf.GetHTTPClient()
}
// AddUpdateDomainRecords 添加或更新IPv4/IPv6记录
func (eranet *Eranet) AddUpdateDomainRecords() config.Domains {
eranet.addUpdateDomainRecords("A")
eranet.addUpdateDomainRecords("AAAA")
return eranet.Domains
}
func (eranet *Eranet) addUpdateDomainRecords(recordType string) {
ipAddr, domains := eranet.Domains.GetNewIpResult(recordType)
if ipAddr == "" {
return
}
for _, domain := range domains {
result, err := eranet.getRecordList(domain, recordType)
if err != nil {
util.Log("查询域名信息发生异常! %s", err)
domain.UpdateStatus = config.UpdatedFailed
return
}
if len(result.Data) > 0 {
// 默认第一个
recordSelected := result.Data[0]
params := domain.GetCustomParams()
if params.Has("Id") {
for i := 0; i < len(result.Data); i++ {
if strconv.Itoa(result.Data[i].ID) == params.Get("Id") {
recordSelected = result.Data[i]
}
}
}
// 更新
eranet.modify(recordSelected, domain, recordType, ipAddr)
} else {
// 新增
eranet.create(domain, recordType, ipAddr)
}
}
}
// create 创建DNS记录
func (eranet *Eranet) create(domain *config.Domain, recordType string, ipAddr string) {
param := map[string]string{
"Domain": domain.DomainName,
"Host": domain.GetSubDomain(),
"Type": recordType,
"Value": ipAddr,
"Ttl": eranet.TTL,
}
res, err := eranet.request("/api/Dns/AddDomainRecord", param, "GET")
if err != nil {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, err.Error())
domain.UpdateStatus = config.UpdatedFailed
}
var result NowcnBaseResult
err = json.Unmarshal(res, &result)
if err != nil {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, err.Error())
domain.UpdateStatus = config.UpdatedFailed
}
if result.Error != "" {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, result.Error)
domain.UpdateStatus = config.UpdatedFailed
} else {
domain.UpdateStatus = config.UpdatedSuccess
}
}
// modify 修改DNS记录
func (eranet *Eranet) modify(record EranetRecord, domain *config.Domain, recordType string, ipAddr string) {
// 相同不修改
if record.Value == ipAddr {
util.Log("你的IP %s 没有变化, 域名 %s", ipAddr, domain)
return
}
param := map[string]string{
"Id": strconv.Itoa(record.ID),
"Domain": domain.DomainName,
"Host": domain.GetSubDomain(),
"Type": recordType,
"Value": ipAddr,
"Ttl": eranet.TTL,
}
res, err := eranet.request("/api/Dns/UpdateDomainRecord", param, "GET")
if err != nil {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err.Error())
domain.UpdateStatus = config.UpdatedFailed
}
var result NowcnBaseResult
err = json.Unmarshal(res, &result)
if err != nil {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err.Error())
domain.UpdateStatus = config.UpdatedFailed
}
if result.Error != "" {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, result.Error)
domain.UpdateStatus = config.UpdatedFailed
} else {
util.Log("更新域名解析 %s 成功! IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
}
}
// getRecordList 获取域名记录列表
func (eranet *Eranet) getRecordList(domain *config.Domain, typ string) (result EranetRecordListResp, err error) {
param := map[string]string{
"Domain": domain.DomainName,
"Type": typ,
"Host": domain.GetSubDomain(),
}
res, err := eranet.request("/api/Dns/DescribeRecordIndex", param, "GET")
err = json.Unmarshal(res, &result)
return
}
func (eranet *Eranet) queryParams(param map[string]any) string {
var queryParams []string
for key, value := range param {
// 只对键进行URL编码,值保持原样(特别是@符号)
encodedKey := url.QueryEscape(key)
valueStr := fmt.Sprintf("%v", value)
// 对值进行选择性编码,保留@符号
encodedValue := strings.ReplaceAll(url.QueryEscape(valueStr), "%40", "@")
encodedValue = strings.ReplaceAll(encodedValue, "%3A", ":")
queryParams = append(queryParams, encodedKey+"="+encodedValue)
}
return strings.Join(queryParams, "&")
}
func (t *Eranet) sign(params map[string]string, method string) (string, error) {
// 添加公共参数
params["AccessInstanceID"] = t.DNS.ID
params["SignatureMethod"] = "HMAC-SHA1"
params["SignatureNonce"] = fmt.Sprintf("%d", time.Now().UnixNano())
params["Timestamp"] = time.Now().UTC().Format("2006-01-02T15:04:05Z")
// 1. 排序参数(按首字母顺序)
var keys []string
for k := range params {
if k != "Signature" { // 排除Signature参数
keys = append(keys, k)
}
}
sort.Strings(keys)
// 2. 构造规范化请求字符串
var canonicalizedQuery []string
for _, k := range keys {
// URL编码参数名和参数值
encodedKey := util.PercentEncode(k)
encodedValue := util.PercentEncode(params[k])
canonicalizedQuery = append(canonicalizedQuery, encodedKey+"="+encodedValue)
}
canonicalizedQueryString := strings.Join(canonicalizedQuery, "&")
// 3. 构造待签名字符串
stringToSign := method + "&" + util.PercentEncode("/") + "&" + util.PercentEncode(canonicalizedQueryString)
// 4. 计算HMAC-SHA1签名
key := t.DNS.Secret + "&"
h := hmac.New(sha1.New, []byte(key))
h.Write([]byte(stringToSign))
signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
// 5. 添加签名到参数中
params["Signature"] = signature
// 6. 重新构造最终的查询字符串(包含签名)
keys = append(keys, "Signature")
sort.Strings(keys)
var finalQuery []string
for _, k := range keys {
encodedKey := util.PercentEncode(k)
encodedValue := util.PercentEncode(params[k])
finalQuery = append(finalQuery, encodedKey+"="+encodedValue)
}
return strings.Join(finalQuery, "&"), nil
}
func (t *Eranet) request(apiPath string, params map[string]string, method string) ([]byte, error) {
// 生成签名
queryString, err := t.sign(params, method)
if err != nil {
return nil, fmt.Errorf("生成签名失败: %v", err)
}
// 构造完整URL
baseURL := "https://www.eranet.com"
fullURL := baseURL + apiPath + "?" + queryString
// 创建HTTP请求
req, err := http.NewRequest(method, fullURL, nil)
if err != nil {
return nil, fmt.Errorf("创建请求失败: %v", err)
}
// 设置请求头
req.Header.Set("Accept", "application/json")
// 发送请求
client := t.httpClient
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("请求失败: %v", err)
}
defer resp.Body.Close()
// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应失败: %v", err)
}
// 检查HTTP状态码
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API请求失败,状态码: %d, 响应: %s", resp.StatusCode, string(body))
}
return body, nil
}
================================================
FILE: dns/gcore.go
================================================
package dns
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"github.com/jeessy2/ddns-go/v6/config"
"github.com/jeessy2/ddns-go/v6/util"
)
const gcoreAPIEndpoint = "https://api.gcore.com/dns/v2"
// Gcore Gcore DNS实现
type Gcore struct {
DNS config.DNS
Domains config.Domains
TTL int
httpClient *http.Client
}
// GcoreZoneResponse zones返回结果
type GcoreZoneResponse struct {
Zones []GcoreZone `json:"zones"`
TotalAmount int `json:"total_amount"`
}
// GcoreZone 域名信息
type GcoreZone struct {
ID int `json:"id"`
Name string `json:"name"`
}
// GcoreRRSetListResponse RRSet列表返回结果
type GcoreRRSetListResponse struct {
RRSets []GcoreRRSet `json:"rrsets"`
TotalAmount int `json:"total_amount"`
}
// GcoreRRSet RRSet记录实体
type GcoreRRSet struct {
Name string `json:"name"`
Type string `json:"type"`
TTL int `json:"ttl"`
ResourceRecords []GcoreResourceRecord `json:"resource_records"`
Meta map[string]interface{} `json:"meta,omitempty"`
}
// GcoreResourceRecord 资源记录
type GcoreResourceRecord struct {
Content []interface{} `json:"content"`
Enabled bool `json:"enabled"`
ID int `json:"id,omitempty"`
Meta map[string]interface{} `json:"meta,omitempty"`
}
// GcoreInputRRSet 输入的RRSet
type GcoreInputRRSet struct {
TTL int `json:"ttl"`
ResourceRecords []GcoreInputResourceRecord `json:"resource_records"`
Meta map[string]interface{} `json:"meta,omitempty"`
}
// GcoreInputResourceRecord 输入的资源记录
type GcoreInputResourceRecord struct {
Content []interface{} `json:"content"`
Enabled bool `json:"enabled"`
Meta map[string]interface{} `json:"meta,omitempty"`
}
// Init 初始化
func (gc *Gcore) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpCache, ipv6cache *util.IpCache) {
gc.Domains.Ipv4Cache = ipv4cache
gc.Domains.Ipv6Cache = ipv6cache
gc.DNS = dnsConf.DNS
gc.Domains.GetNewIp(dnsConf)
if dnsConf.TTL == "" {
// 默认 120 秒(免费版最低值)
gc.TTL = 120
} else {
ttl, err := strconv.Atoi(dnsConf.TTL)
if err != nil {
gc.TTL = 120
} else {
gc.TTL = ttl
}
}
gc.httpClient = dnsConf.GetHTTPClient()
}
// AddUpdateDomainRecords 添加或更新 IPv4 / IPv6 记录
func (gc *Gcore) AddUpdateDomainRecords() config.Domains {
gc.addUpdateDomainRecords("A")
gc.addUpdateDomainRecords("AAAA")
return gc.Domains
}
func (gc *Gcore) addUpdateDomainRecords(recordType string) {
ipAddr, domains := gc.Domains.GetNewIpResult(recordType)
if ipAddr == "" {
return
}
for _, domain := range domains {
// get zone
zoneInfo, err := gc.getZoneByDomain(domain)
if err != nil {
util.Log("查询域名信息发生异常! %s", err)
domain.UpdateStatus = config.UpdatedFailed
continue
}
if zoneInfo == nil {
util.Log("在DNS服务商中未找到根域名: %s", domain.DomainName)
domain.UpdateStatus = config.UpdatedFailed
continue
}
// 查询现有记录
existingRecord, err := gc.getRRSet(zoneInfo.Name, domain.GetSubDomain(), recordType)
if err != nil {
util.Log("查询域名信息发生异常! %s", err)
domain.UpdateStatus = config.UpdatedFailed
continue
}
if existingRecord != nil {
// 更新现有记录
gc.updateRecord(zoneInfo.Name, domain, recordType, ipAddr, existingRecord)
} else {
// 创建新记录
gc.createRecord(zoneInfo.Name, domain, recordType, ipAddr)
}
}
}
// 获取域名对应的Zone信息
func (gc *Gcore) getZoneByDomain(domain *config.Domain) (*GcoreZone, error) {
var result GcoreZoneResponse
params := url.Values{}
params.Set("name", domain.DomainName)
err := gc.request(
"GET",
fmt.Sprintf("%s/zones?%s", gcoreAPIEndpoint, params.Encode()),
nil,
&result,
)
if err != nil {
return nil, err
}
if len(result.Zones) > 0 {
return &result.Zones[0], nil
}
return nil, nil
}
// 获取指定的RRSet记录
func (gc *Gcore) getRRSet(zoneName, recordName, recordType string) (*GcoreRRSet, error) {
var result GcoreRRSetListResponse
err := gc.request(
"GET",
fmt.Sprintf("%s/zones/%s/rrsets", gcoreAPIEndpoint, zoneName),
nil,
&result,
)
if err != nil {
return nil, err
}
// 查找匹配的记录
fullRecordName := recordName
if recordName != "" && recordName != "@" {
fullRecordName = recordName + "." + zoneName
} else {
fullRecordName = zoneName
}
for _, rrset := range result.RRSets {
if rrset.Name == fullRecordName && rrset.Type == recordType {
return &rrset, nil
}
}
return nil, nil
}
// 创建新记录
func (gc *Gcore) createRecord(zoneName string, domain *config.Domain, recordType string, ipAddr string) {
recordName := domain.GetSubDomain()
if recordName == "" || recordName == "@" {
recordName = zoneName
} else {
recordName = recordName + "." + zoneName
}
inputRRSet := GcoreInputRRSet{
TTL: gc.TTL,
ResourceRecords: []GcoreInputResourceRecord{
{
Content: []interface{}{ipAddr},
Enabled: true,
},
},
}
var result interface{}
err := gc.request(
"POST",
fmt.Sprintf("%s/zones/%s/%s/%s", gcoreAPIEndpoint, zoneName, recordName, recordType),
inputRRSet,
&result,
)
if err != nil {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, err)
domain.UpdateStatus = config.UpdatedFailed
return
}
util.Log("新增域名解析 %s 成功! IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
}
// 更新现有记录
func (gc *Gcore) updateRecord(zoneName string, domain *config.Domain, recordType string, ipAddr string, existingRecord *GcoreRRSet) {
// 检查IP是否相同
if len(existingRecord.ResourceRecords) > 0 && len(existingRecord.ResourceRecords[0].Content) > 0 {
if existingRecord.ResourceRecords[0].Content[0] == ipAddr {
util.Log("你的IP %s 没有变化, 域名 %s", ipAddr, domain)
return
}
}
recordName := domain.GetSubDomain()
if recordName == "" || recordName == "@" {
recordName = zoneName
} else {
recordName = recordName + "." + zoneName
}
inputRRSet := GcoreInputRRSet{
TTL: gc.TTL,
ResourceRecords: []GcoreInputResourceRecord{
{
Content: []interface{}{ipAddr},
Enabled: true,
},
},
}
var result interface{}
err := gc.request(
"PUT",
fmt.Sprintf("%s/zones/%s/%s/%s", gcoreAPIEndpoint, zoneName, recordName, recordType),
inputRRSet,
&result,
)
if err != nil {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err)
domain.UpdateStatus = config.UpdatedFailed
return
}
util.Log("更新域名解析 %s 成功! IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
}
// request 统一请求接口
func (gc *Gcore) request(method string, url string, data interface{}, result interface{}) (err error) {
jsonStr := make([]byte, 0)
if data != nil {
jsonStr, _ = json.Marshal(data)
}
req, err := http.NewRequest(
method,
url,
bytes.NewBuffer(jsonStr),
)
if err != nil {
return
}
req.Header.Set("Authorization", "APIKey "+gc.DNS.Secret)
req.Header.Set("Content-Type", "application/json")
client := gc.httpClient
resp, err := client.Do(req)
err = util.GetHTTPResponse(resp, err, result)
return
}
================================================
FILE: dns/godaddy.go
================================================
package dns
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"strconv"
"github.com/jeessy2/ddns-go/v6/config"
"github.com/jeessy2/ddns-go/v6/util"
)
type godaddyRecord struct {
Data string `json:"data"`
Name string `json:"name"`
TTL int `json:"ttl"`
Type string `json:"type"`
}
type godaddyRecords []godaddyRecord
type GoDaddyDNS struct {
dns config.DNS
domains config.Domains
ttl int
header http.Header
client *http.Client
lastIpv4 string
lastIpv6 string
}
func (g *GoDaddyDNS) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpCache, ipv6cache *util.IpCache) {
g.domains.Ipv4Cache = ipv4cache
g.domains.Ipv6Cache = ipv6cache
g.lastIpv4 = ipv4cache.Addr
g.lastIpv6 = ipv6cache.Addr
g.dns = dnsConf.DNS
g.domains.GetNewIp(dnsConf)
g.ttl = 600
if val, err := strconv.Atoi(dnsConf.TTL); err == nil {
g.ttl = val
}
g.header = map[string][]string{
"Authorization": {fmt.Sprintf("sso-key %s:%s", g.dns.ID, g.dns.Secret)},
"Content-Type": {"application/json"},
}
g.client = dnsConf.GetHTTPClient()
}
func (g *GoDaddyDNS) updateDomainRecord(recordType string, ipAddr string, domains []*config.Domain) {
if ipAddr == "" {
return
}
// 防止多次发送Webhook通知
if recordType == "A" {
if g.lastIpv4 == ipAddr {
util.Log("你的IPv4未变化, 未触发 %s 请求", "godaddy")
return
}
} else {
if g.lastIpv6 == ipAddr {
util.Log("你的IPv6未变化, 未触发 %s 请求", "godaddy")
return
}
}
for _, domain := range domains {
err := g.sendReq(http.MethodPut, recordType, domain, &godaddyRecords{godaddyRecord{
Data: ipAddr,
Name: domain.GetSubDomain(),
TTL: g.ttl,
Type: recordType,
}})
if err == nil {
util.Log("更新域名解析 %s 成功! IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
} else {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err)
domain.UpdateStatus = config.UpdatedFailed
}
}
}
func (g *GoDaddyDNS) AddUpdateDomainRecords() config.Domains {
if ipv4Addr, ipv4Domains := g.domains.GetNewIpResult("A"); ipv4Addr != "" {
g.updateDomainRecord("A", ipv4Addr, ipv4Domains)
}
if ipv6Addr, ipv6Domains := g.domains.GetNewIpResult("AAAA"); ipv6Addr != "" {
g.updateDomainRecord("AAAA", ipv6Addr, ipv6Domains)
}
return g.domains
}
func (g *GoDaddyDNS) sendReq(method string, rType string, domain *config.Domain, data *godaddyRecords) error {
var body *bytes.Buffer
if data != nil {
if buffer, err := json.Marshal(data); err != nil {
return err
} else {
body = bytes.NewBuffer(buffer)
}
}
path := fmt.Sprintf("https://api.godaddy.com/v1/domains/%s/records/%s/%s",
domain.DomainName, rType, domain.GetSubDomain())
req, err := http.NewRequest(method, path, body)
if err != nil {
return err
}
req.Header = g.header
resp, err := g.client.Do(req)
_, err = util.GetHTTPResponseOrg(resp, err)
return err
}
================================================
FILE: dns/huawei.go
================================================
package dns
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"github.com/jeessy2/ddns-go/v6/config"
"github.com/jeessy2/ddns-go/v6/util"
)
const (
huaweicloudEndpoint string = "https://dns.myhuaweicloud.com"
)
// https://support.huaweicloud.com/api-dns/dns_api_64001.html
// Huaweicloud Huaweicloud
type Huaweicloud struct {
DNS config.DNS
Domains config.Domains
TTL int
httpClient *http.Client
}
// HuaweicloudZonesResp zones response
type HuaweicloudZonesResp struct {
Zones []struct {
ID string
Name string
Recordsets []HuaweicloudRecordsets
}
}
// HuaweicloudRecordsResp 记录返回结果
type HuaweicloudRecordsResp struct {
Recordsets []HuaweicloudRecordsets
}
// HuaweicloudRecordsets 记录
type HuaweicloudRecordsets struct {
ID string
Name string `json:"name"`
ZoneID string `json:"zone_id"`
Status string
Type string `json:"type"`
TTL int `json:"ttl"`
Records []string `json:"records"`
Weight int `json:"weight"`
}
// Init 初始化
func (hw *Huaweicloud) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpCache, ipv6cache *util.IpCache) {
hw.Domains.Ipv4Cache = ipv4cache
hw.Domains.Ipv6Cache = ipv6cache
hw.DNS = dnsConf.DNS
hw.Domains.GetNewIp(dnsConf)
if dnsConf.TTL == "" {
// 默认300s
hw.TTL = 300
} else {
ttl, err := strconv.Atoi(dnsConf.TTL)
if err != nil {
hw.TTL = 300
} else {
hw.TTL = ttl
}
}
hw.httpClient = dnsConf.GetHTTPClient()
}
// AddUpdateDomainRecords 添加或更新IPv4/IPv6记录
func (hw *Huaweicloud) AddUpdateDomainRecords() config.Domains {
hw.addUpdateDomainRecords("A")
hw.addUpdateDomainRecords("AAAA")
return hw.Domains
}
func (hw *Huaweicloud) addUpdateDomainRecords(recordType string) {
ipAddr, domains := hw.Domains.GetNewIpResult(recordType)
if ipAddr == "" {
return
}
for _, domain := range domains {
customParams := domain.GetCustomParams()
params := url.Values{}
params.Set("name", domain.String())
params.Set("type", recordType)
// 如果有精准匹配
// 详见 查询记录集 https://support.huaweicloud.com/api-dns/dns_api_64002.html
if customParams.Has("zone_id") && customParams.Has("recordset_id") {
var record HuaweicloudRecordsets
err := hw.request(
"GET",
fmt.Sprintf(huaweicloudEndpoint+"/v2.1/zones/%s/recordsets/%s", customParams.Get("zone_id"), customParams.Get("recordset_id")),
params,
&record,
)
if err != nil {
util.Log("查询域名信息发生异常! %s", err)
domain.UpdateStatus = config.UpdatedFailed
return
}
// 更新
hw.modify(record, domain, ipAddr)
} else { // 没有精准匹配,则支持更多的查询参数。详见 查询租户记录集列表 https://support.huaweicloud.com/api-dns/dns_api_64003.html
// 复制所有自定义参数
util.CopyUrlParams(customParams, params, nil)
// 参数名修正
if params.Has("recordset_id") {
params.Set("id", params.Get("recordset_id"))
params.Del("recordset_id")
}
var records HuaweicloudRecordsResp
err := hw.request(
"GET",
huaweicloudEndpoint+"/v2.1/recordsets",
params,
&records,
)
if err != nil {
util.Log("查询域名信息发生异常! %s", err)
domain.UpdateStatus = config.UpdatedFailed
return
}
find := false
for _, record := range records.Recordsets {
// 名称相同才更新。华为云默认是模糊搜索
if record.Name == domain.String()+"." {
// 更新
hw.modify(record, domain, ipAddr)
find = true
break
}
}
if !find {
thIdParamName := ""
if customParams.Has("id") {
thIdParamName = "id"
} else if customParams.Has("recordset_id") {
thIdParamName = "recordset_id"
}
if thIdParamName != "" {
util.Log("域名 %s 解析未找到,且因添加了参数 %s=%s 导致无法创建。本次更新已被忽略", domain, thIdParamName, customParams.Get(thIdParamName))
} else {
// 新增
hw.create(domain, recordType, ipAddr)
}
}
}
}
}
// 创建
func (hw *Huaweicloud) create(domain *config.Domain, recordType string, ipAddr string) {
zone, err := hw.getZones(domain)
if err != nil {
util.Log("查询域名信息发生异常! %s", err)
domain.UpdateStatus = config.UpdatedFailed
return
}
if len(zone.Zones) == 0 {
util.Log("在DNS服务商中未找到根域名: %s", domain.DomainName)
domain.UpdateStatus = config.UpdatedFailed
return
}
zoneID := zone.Zones[0].ID
for _, z := range zone.Zones {
if z.Name == domain.DomainName+"." {
zoneID = z.ID
break
}
}
record := &HuaweicloudRecordsets{
Type: recordType,
Name: domain.String() + ".",
Records: []string{ipAddr},
TTL: hw.TTL,
Weight: 1,
}
var result HuaweicloudRecordsets
err = hw.request(
"POST",
fmt.Sprintf(huaweicloudEndpoint+"/v2.1/zones/%s/recordsets", zoneID),
record,
&result,
)
if err != nil {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, err)
domain.UpdateStatus = config.UpdatedFailed
return
}
if len(result.Records) > 0 && result.Records[0] == ipAddr {
util.Log("新增域名解析 %s 成功! IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
} else {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, result.Status)
domain.UpdateStatus = config.UpdatedFailed
}
}
// 修改
func (hw *Huaweicloud) modify(record HuaweicloudRecordsets, domain *config.Domain, ipAddr string) {
// 相同不修改
if len(record.Records) > 0 && record.Records[0] == ipAddr {
util.Log("你的IP %s 没有变化, 域名 %s", ipAddr, domain)
return
}
var request = make(map[string]interface{})
request["name"] = record.Name
request["type"] = record.Type
request["records"] = []string{ipAddr}
request["ttl"] = hw.TTL
var result HuaweicloudRecordsets
err := hw.request(
"PUT",
fmt.Sprintf(huaweicloudEndpoint+"/v2.1/zones/%s/recordsets/%s", record.ZoneID, record.ID),
&request,
&result,
)
if err != nil {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err)
domain.UpdateStatus = config.UpdatedFailed
return
}
if len(result.Records) > 0 && result.Records[0] == ipAddr {
util.Log("更新域名解析 %s 成功! IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
} else {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, result.Status)
domain.UpdateStatus = config.UpdatedFailed
}
}
// 获得域名记录列表
func (hw *Huaweicloud) getZones(domain *config.Domain) (result HuaweicloudZonesResp, err error) {
err = hw.request(
"GET",
huaweicloudEndpoint+"/v2/zones",
url.Values{"name": []string{domain.DomainName}},
&result,
)
return
}
// request 统一请求接口
func (hw *Huaweicloud) request(method string, urlString string, data interface{}, result interface{}) (err error) {
var (
req *http.Request
)
if method == "GET" {
req, err = http.NewRequest(
method,
urlString,
bytes.NewBuffer(nil),
)
req.URL.RawQuery = data.(url.Values).Encode()
} else {
jsonStr := make([]byte, 0)
if data != nil {
jsonStr, _ = json.Marshal(data)
}
req, err = http.NewRequest(
method,
urlString,
bytes.NewBuffer(jsonStr),
)
}
if err != nil {
return
}
s := util.Signer{
Key: hw.DNS.ID,
Secret: hw.DNS.Secret,
}
s.Sign(req)
req.Header.Add("content-type", "application/json")
client := hw.httpClient
resp, err := client.Do(req)
err = util.GetHTTPResponse(resp, err, result)
return
}
================================================
FILE: dns/index.go
================================================
package dns
import (
"time"
"github.com/jeessy2/ddns-go/v6/config"
"github.com/jeessy2/ddns-go/v6/util"
)
// DNS interface
type DNS interface {
Init(dnsConf *config.DnsConfig, ipv4cache *util.IpCache, ipv6cache *util.IpCache)
// 添加或更新IPv4/IPv6记录
AddUpdateDomainRecords() (domains config.Domains)
}
var (
Addresses = []string{
alidnsEndpoint,
aliesaEndpoint,
baiduEndpoint,
zonesAPI,
recordListAPI,
huaweicloudEndpoint,
nameCheapEndpoint,
nameSiloListRecordEndpoint,
porkbunEndpoint,
tencentCloudEndPoint,
dynadotEndpoint,
dynv6Endpoint,
gcoreAPIEndpoint,
edgeoneEndPoint,
rainyunEndpoint,
}
Ipcache = [][2]util.IpCache{}
)
// RunTimer 定时运行
func RunTimer(delay time.Duration) {
for {
RunOnce()
time.Sleep(delay)
}
}
// RunOnce RunOnce
func RunOnce() {
conf, err := config.GetConfigCached()
if err != nil {
return
}
if util.ForceCompareGlobal || len(Ipcache) != len(conf.DnsConf) {
Ipcache = [][2]util.IpCache{}
for range conf.DnsConf {
Ipcache = append(Ipcache, [2]util.IpCache{{}, {}})
}
}
for i, dc := range conf.DnsConf {
var dnsSelected DNS
switch dc.DNS.Name {
case "alidns":
dnsSelected = &Alidns{}
case "aliesa":
dnsSelected = &Aliesa{}
case "tencentcloud":
dnsSelected = &TencentCloud{}
case "trafficroute":
dnsSelected = &TrafficRoute{}
case "dnspod":
dnsSelected = &Dnspod{}
case "dnsla":
dnsSelected = &Dnsla{}
case "cloudflare":
dnsSelected = &Cloudflare{}
case "huaweicloud":
dnsSelected = &Huaweicloud{}
case "callback":
dnsSelected = &Callback{}
case "baiducloud":
dnsSelected = &BaiduCloud{}
case "porkbun":
dnsSelected = &Porkbun{}
case "godaddy":
dnsSelected = &GoDaddyDNS{}
case "namecheap":
dnsSelected = &NameCheap{}
case "namesilo":
dnsSelected = &NameSilo{}
case "vercel":
dnsSelected = &Vercel{}
case "dynadot":
dnsSelected = &Dynadot{}
case "dynv6":
dnsSelected = &Dynv6{}
case "spaceship":
dnsSelected = &Spaceship{}
case "nowcn":
dnsSelected = &Nowcn{}
case "eranet":
dnsSelected = &Eranet{}
case "gcore":
dnsSelected = &Gcore{}
case "edgeone":
dnsSelected = &EdgeOne{}
case "nsone":
dnsSelected = &NSOne{}
case "name_com":
dnsSelected = &NameCom{}
case "rainyun":
dnsSelected = &Rainyun{}
default:
dnsSelected = &Alidns{}
}
dnsSelected.Init(&dc, &Ipcache[i][0], &Ipcache[i][1])
domains := dnsSelected.AddUpdateDomainRecords()
// webhook
v4Status, v6Status := config.ExecWebhook(&domains, &conf)
// 重置单个cache
if v4Status == config.UpdatedFailed {
Ipcache[i][0] = util.IpCache{}
}
if v6Status == config.UpdatedFailed {
Ipcache[i][1] = util.IpCache{}
}
}
util.ForceCompareGlobal = false
}
================================================
FILE: dns/name_com.go
================================================
package dns
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/jeessy2/ddns-go/v6/config"
"github.com/jeessy2/ddns-go/v6/util"
)
const (
listRecords = "https://api.name.com/core/v1/domains/%s/records"
createRecord = "https://api.name.com/core/v1/domains/%s/records"
updateRecord = "https://api.name.com/core/v1/domains/%s/records/%d"
)
type NameCom struct {
DNS config.DNS
Domains config.Domains
TTL string
httpClient *http.Client
}
type NameComRecord struct {
TTL int `json:"ttl"`
Type string `json:"type"`
Answer string `json:"answer"`
Host string `json:"host"`
}
type NameComRecordResp struct {
TTL int `json:"ttl"`
Type string `json:"type"`
Answer string `json:"answer"`
DomainName string `json:"domainName"`
Fqdn string `json:"fqdn"`
Host string `json:"host"`
Id int `json:"id"`
Priority int `json:"priority"`
}
type NameComRecordListResp struct {
TotalCount int `json:"totalCount"`
From int `json:"from"`
To int `json:"to"`
Records []NameComRecordResp `json:"records"`
LastPage int `json:"lastPage"`
NextPage int `json:"nextPage"`
}
func (n *NameCom) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpCache, ipv6cache *util.IpCache) {
n.Domains.Ipv4Cache = ipv4cache
n.Domains.Ipv6Cache = ipv6cache
n.DNS = dnsConf.DNS
n.Domains.GetNewIp(dnsConf)
if dnsConf.TTL == "" {
n.TTL = "300"
} else {
n.TTL = dnsConf.TTL
}
n.httpClient = dnsConf.GetHTTPClient()
}
func (n *NameCom) AddUpdateDomainRecords() (domains config.Domains) {
n.addUpdateDomainRecords("A")
n.addUpdateDomainRecords("AAAA")
domains = n.Domains
return
}
func (n *NameCom) addUpdateDomainRecords(recordType string) {
ipAddr, domains := n.Domains.GetNewIpResult(recordType)
if ipAddr == "" {
return
}
for _, domain := range domains {
resp, err := n.getRecordList(domain)
if err != nil {
util.Log("查询域名信息发生异常! %s", err)
domain.UpdateStatus = config.UpdatedFailed
return
}
resp4TypeRecords := make([]NameComRecordResp, 0, resp.TotalCount)
if resp.TotalCount > 0 {
for _, r := range resp.Records {
if r.Type == recordType && r.Host == domain.SubDomain {
resp4TypeRecords = append(resp4TypeRecords, r)
}
}
}
if len(resp4TypeRecords) > 0 {
for _, r := range resp4TypeRecords {
err := n.update(r, domain, ipAddr, recordType)
if err != nil {
domain.UpdateStatus = config.UpdatedFailed
return
}
}
} else {
_, err := n.create(domain, recordType, ipAddr)
if err != nil {
domain.UpdateStatus = config.UpdatedFailed
return
}
}
}
}
func (n *NameCom) getRecordList(domain *config.Domain) (resp *NameComRecordListResp, err error) {
url := fmt.Sprintf(listRecords, domain.DomainName)
err = n.request("GET", url, nil, &resp)
return
}
func (n *NameCom) create(domain *config.Domain, recordType string, ipAddr string) (resp *NameComRecord, err error) {
i, err := strconv.Atoi(n.TTL)
if err != nil {
return
}
resq := &NameComRecord{
TTL: i,
Answer: ipAddr,
Host: domain.SubDomain,
Type: recordType,
}
url := fmt.Sprintf(createRecord, domain.DomainName)
err = n.request("POST", url, resq, resp)
if err != nil {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, err)
return
}
util.Log("新增域名解析 %s 成功! IP: %s", domain, ipAddr)
return
}
func (n *NameCom) update(record NameComRecordResp, domain *config.Domain, ipAddr, recordType string) (err error) {
if record.Answer == ipAddr {
util.Log("你的IP %s 没有变化, 域名 %s", ipAddr, domain)
return
}
record.Answer = ipAddr
record.Type = recordType
url := fmt.Sprintf(updateRecord, domain.DomainName, record.Id)
err = n.request("PUT", url, record, nil)
if err != nil {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err)
return
}
util.Log("更新域名解析 %s 成功! IP: %s", domain, ipAddr)
return
}
func (n *NameCom) request(action string, url string, data any, result any) (err error) {
jsonStr := make([]byte, 0)
if data != nil {
jsonStr, err = json.Marshal(data)
if err != nil {
return
}
}
req, err := http.NewRequest(
action,
url,
bytes.NewBuffer(jsonStr),
)
if err != nil {
return
}
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(n.DNS.ID+":"+n.DNS.Secret)))
if strings.EqualFold(action, "POST") || strings.EqualFold(action, "PUT") {
req.Header.Add("Content-Type", "application/json")
}
client := n.httpClient
resp, err := client.Do(req)
err = util.GetHTTPResponse(resp, err, result)
return
}
================================================
FILE: dns/namecheap.go
================================================
package dns
import (
"io"
"net/http"
"strings"
"github.com/jeessy2/ddns-go/v6/config"
"github.com/jeessy2/ddns-go/v6/util"
)
const (
nameCheapEndpoint string = "https://dynamicdns.park-your-domain.com/update?host=#{host}&domain=#{domain}&password=#{password}&ip=#{ip}"
)
// NameCheap Domain
type NameCheap struct {
DNS config.DNS
Domains config.Domains
lastIpv4 string
lastIpv6 string
httpClient *http.Client
}
// NameCheap 修改域名解析结果
type NameCheapResp struct {
Status string
Errors []string
}
// Init 初始化
func (nc *NameCheap) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpCache, ipv6cache *util.IpCache) {
nc.Domains.Ipv4Cache = ipv4cache
nc.Domains.Ipv6Cache = ipv6cache
nc.lastIpv4 = ipv4cache.Addr
nc.lastIpv6 = ipv6cache.Addr
nc.DNS = dnsConf.DNS
nc.Domains.GetNewIp(dnsConf)
nc.httpClient = dnsConf.GetHTTPClient()
}
// AddUpdateDomainRecords 添加或更新IPv4/IPv6记录
func (nc *NameCheap) AddUpdateDomainRecords() config.Domains {
nc.addUpdateDomainRecords("A")
nc.addUpdateDomainRecords("AAAA")
return nc.Domains
}
func (nc *NameCheap) addUpdateDomainRecords(recordType string) {
ipAddr, domains := nc.Domains.GetNewIpResult(recordType)
if ipAddr == "" {
return
}
// 防止多次发送Webhook通知
if recordType == "A" {
if nc.lastIpv4 == ipAddr {
util.Log("你的IPv4未变化, 未触发 %s 请求", "NameCheap")
return
}
} else {
// https://www.namecheap.com/support/knowledgebase/article.aspx/29/11/how-to-dynamically-update-the-hosts-ip-with-an-http-request/
util.Log("Namecheap 不支持更新 IPv6")
return
}
for _, domain := range domains {
nc.modify(domain, ipAddr)
}
}
// 修改
func (nc *NameCheap) modify(domain *config.Domain, ipAddr string) {
var result NameCheapResp
err := nc.request(&result, ipAddr, domain)
if err != nil {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err)
domain.UpdateStatus = config.UpdatedFailed
return
}
switch result.Status {
case "Success":
util.Log("更新域名解析 %s 成功! IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
default:
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, result.Status)
domain.UpdateStatus = config.UpdatedFailed
}
}
// request 统一请求接口
func (nc *NameCheap) request(result *NameCheapResp, ipAddr string, domain *config.Domain) (err error) {
url := strings.NewReplacer(
"#{host}", domain.GetSubDomain(),
"#{domain}", domain.DomainName,
"#{password}", nc.DNS.Secret,
"#{ip}", ipAddr,
).Replace(nameCheapEndpoint)
req, err := http.NewRequest(
http.MethodGet,
url,
http.NoBody,
)
if err != nil {
return
}
client := nc.httpClient
resp, err := client.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
status := string(data)
if strings.Contains(status, "<ErrCount>0</ErrCount>") {
result.Status = "Success"
} else {
result.Status = status
}
return
}
================================================
FILE: dns/namesilo.go
================================================
package dns
import (
"encoding/xml"
"io"
"net/http"
"strings"
"github.com/jeessy2/ddns-go/v6/config"
"github.com/jeessy2/ddns-go/v6/util"
)
const (
nameSiloListRecordEndpoint = "https://www.namesilo.com/api/dnsListRecords?version=1&type=xml&key=#{password}&domain=#{domain}"
nameSiloAddRecordEndpoint = "https://www.namesilo.com/api/dnsAddRecord?version=1&type=xml&key=#{password}&domain=#{domain}&rrhost=#{host}&rrtype=#{recordType}&rrvalue=#{ip}&rrttl=3600"
nameSiloUpdateRecordEndpoint = "https://www.namesilo.com/api/dnsUpdateRecord?version=1&type=xml&key=#{password}&domain=#{domain}&rrhost=#{host}&rrid=#{recordID}&rrvalue=#{ip}&rrttl=3600"
)
// NameSilo Domain
type NameSilo struct {
DNS config.DNS
Domains config.Domains
lastIpv4 string
lastIpv6 string
httpClient *http.Client
}
// NameSiloResp 修改域名解析结果
type NameSiloResp struct {
XMLName xml.Name `xml:"namesilo"`
Request Request `xml:"request"`
Reply ReplyResponse `xml:"reply"`
}
type ReplyResponse struct {
Code int `xml:"code"`
Detail string `xml:"detail"`
RecordID string `xml:"record_id"`
}
type NameSiloDNSListRecordResp struct {
XMLName xml.Name `xml:"namesilo"`
Request Request `xml:"request"`
Reply Reply `xml:"reply"`
}
type Request struct {
Operation string `xml:"operation"`
IP string `xml:"ip"`
}
type Reply struct {
Code int `xml:"code"`
Detail string `xml:"detail"`
ResourceItems []ResourceRecord `xml:"resource_record"`
}
type ResourceRecord struct {
RecordID string `xml:"record_id"`
Type string `xml:"type"`
Host string `xml:"host"`
Value string `xml:"value"`
TTL int `xml:"ttl"`
Distance int `xml:"distance"`
}
// Init 初始化
func (ns *NameSilo) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpCache, ipv6cache *util.IpCache) {
ns.Domains.Ipv4Cache = ipv4cache
ns.Domains.Ipv6Cache = ipv6cache
ns.lastIpv4 = ipv4cache.Addr
ns.lastIpv6 = ipv6cache.Addr
ns.DNS = dnsConf.DNS
ns.Domains.GetNewIp(dnsConf)
ns.httpClient = dnsConf.GetHTTPClient()
}
// AddUpdateDomainRecords 添加或更新IPv4/IPv6记录
func (ns *NameSilo) AddUpdateDomainRecords() config.Domains {
ns.addUpdateDomainRecords("A")
ns.addUpdateDomainRecords("AAAA")
return ns.Domains
}
func (ns *NameSilo) addUpdateDomainRecords(recordType string) {
ipAddr, domains := ns.Domains.GetNewIpResult(recordType)
if ipAddr == "" {
return
}
for _, domain := range domains {
if domain.SubDomain == "" {
domain.SubDomain = "@"
}
// 拿到DNS记录列表,从列表中去取对应域名的id,有id进行修改,没ID进行新增
records, err := ns.listRecords(domain)
if err != nil {
util.Log("查询域名信息发生异常! %s", err)
domain.UpdateStatus = config.UpdatedFailed
return
}
items := records.Reply.ResourceItems
record := findResourceRecord(items, recordType, domain.SubDomain)
var isAdd bool
var recordID string
if record == nil {
isAdd = true
} else {
recordID = record.RecordID
if record.Value == ipAddr {
util.Log("你的IP %s 没有变化, 域名 %s", ipAddr, domain)
continue
}
}
ns.modify(domain, recordID, recordType, ipAddr, isAdd)
}
}
// 修改
func (ns *NameSilo) modify(domain *config.Domain, recordID, recordType, ipAddr string, isAdd bool) {
var err error
var result string
var requestType string
if isAdd {
requestType = "新增"
result, err = ns.request(ipAddr, domain, "", recordType, nameSiloAddRecordEndpoint)
} else {
requestType = "更新"
result, err = ns.request(ipAddr, domain, recordID, "", nameSiloUpdateRecordEndpoint)
}
if err != nil {
util.Log("异常信息: %s", err)
domain.UpdateStatus = config.UpdatedFailed
return
}
var resp NameSiloResp
xml.Unmarshal([]byte(result), &resp)
if resp.Reply.Code == 300 {
util.Log(requestType+"域名解析 %s 成功! IP: %s\n", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
} else {
util.Log(requestType+"域名解析 %s 失败! 异常信息: %s", domain, resp.Reply.Detail)
domain.UpdateStatus = config.UpdatedFailed
}
}
func (ns *NameSilo) listRecords(domain *config.Domain) (*NameSiloDNSListRecordResp, error) {
result, err := ns.request("", domain, "", "", nameSiloListRecordEndpoint)
if err != nil {
return nil, err
}
var resp NameSiloDNSListRecordResp
if err = xml.Unmarshal([]byte(result), &resp); err != nil {
return nil, err
}
return &resp, nil
}
// request 统一请求接口
func (ns *NameSilo) request(ipAddr string, domain *config.Domain, recordID, recordType, url string) (result string, err error) {
url = strings.NewReplacer(
"#{host}", domain.SubDomain,
"#{domain}", domain.DomainName,
"#{password}", ns.DNS.Secret,
"#{recordID}", recordID,
"#{recordType}", recordType,
"#{ip}", ipAddr,
).Replace(url)
req, err := http.NewRequest(
http.MethodGet,
url,
http.NoBody,
)
if err != nil {
return
}
client := ns.httpClient
resp, err := client.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
result = string(data)
return
}
func findResourceRecord(data []ResourceRecord, recordType, domain string) *ResourceRecord {
for i := 0; i < len(data); i++ {
if data[i].Host == domain && data[i].Type == recordType {
return &data[i]
}
}
return nil
}
================================================
FILE: dns/nowcn.go
================================================
package dns
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"sort"
"strconv"
"strings"
"time"
"github.com/jeessy2/ddns-go/v6/config"
"github.com/jeessy2/ddns-go/v6/util"
)
// https://www.todaynic.com/docApi/
// Nowcn nowcn DNS实现
type Nowcn struct {
DNS config.DNS
Domains config.Domains
TTL string
httpClient *http.Client
}
// NowcnRecord DNS记录结构
type NowcnRecord struct {
ID int `json:"id"`
Domain string
Host string
Type string
Value string
State int
// Name string
// Enabled string
}
// NowcnRecordListResp 记录列表响应
type NowcnRecordListResp struct {
NowcnBaseResult
Data []NowcnRecord
}
// NowcnStatus API响应状态
type NowcnBaseResult struct {
RequestId string `json:"RequestId"`
Id int `json:"Id"`
Error string `json:"error"`
}
// Init 初始化
func (nowcn *Nowcn) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpCache, ipv6cache *util.IpCache) {
nowcn.Domains.Ipv4Cache = ipv4cache
nowcn.Domains.Ipv6Cache = ipv6cache
nowcn.DNS = dnsConf.DNS
nowcn.Domains.GetNewIp(dnsConf)
if dnsConf.TTL == "" {
// 默认600s
nowcn.TTL = "600"
} else {
nowcn.TTL = dnsConf.TTL
}
nowcn.httpClient = dnsConf.GetHTTPClient()
}
// AddUpdateDomainRecords 添加或更新IPv4/IPv6记录
func (nowcn *Nowcn) AddUpdateDomainRecords() config.Domains {
nowcn.addUpdateDomainRecords("A")
nowcn.addUpdateDomainRecords("AAAA")
return nowcn.Domains
}
func (nowcn *Nowcn) addUpdateDomainRecords(recordType string) {
ipAddr, domains := nowcn.Domains.GetNewIpResult(recordType)
if ipAddr == "" {
return
}
for _, domain := range domains {
result, err := nowcn.getRecordList(domain, recordType)
if err != nil {
util.Log("查询域名信息发生异常! %s", err)
domain.UpdateStatus = config.UpdatedFailed
return
}
if len(result.Data) > 0 {
// 默认第一个
recordSelected := result.Data[0]
params := domain.GetCustomParams()
if params.Has("Id") {
for i := 0; i < len(result.Data); i++ {
if strconv.Itoa(result.Data[i].ID) == params.Get("Id") {
recordSelected = result.Data[i]
}
}
}
// 更新
nowcn.modify(recordSelected, domain, recordType, ipAddr)
} else {
// 新增
nowcn.create(domain, recordType, ipAddr)
}
}
}
// create 创建DNS记录
func (nowcn *Nowcn) create(domain *config.Domain, recordType string, ipAddr string) {
param := map[string]string{
"Domain": domain.DomainName,
"Host": domain.GetSubDomain(),
"Type": recordType,
"Value": ipAddr,
"Ttl": nowcn.TTL,
}
res, err := nowcn.request("/api/Dns/AddDomainRecord", param, "GET")
if err != nil {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, err.Error())
domain.UpdateStatus = config.UpdatedFailed
}
var result NowcnBaseResult
err = json.Unmarshal(res, &result)
if err != nil {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, err.Error())
domain.UpdateStatus = config.UpdatedFailed
}
if result.Error != "" {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, result.Error)
domain.UpdateStatus = config.UpdatedFailed
} else {
domain.UpdateStatus = config.UpdatedSuccess
}
}
// modify 修改DNS记录
func (nowcn *Nowcn) modify(record NowcnRecord, domain *config.Domain, recordType string, ipAddr string) {
// 相同不修改
if record.Value == ipAddr {
util.Log("你的IP %s 没有变化, 域名 %s", ipAddr, domain)
return
}
param := map[string]string{
"Id": strconv.Itoa(record.ID),
"Domain": domain.DomainName,
"Host": domain.GetSubDomain(),
"Type": recordType,
"Value": ipAddr,
"Ttl": nowcn.TTL,
}
res, err := nowcn.request("/api/Dns/UpdateDomainRecord", param, "GET")
if err != nil {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err.Error())
domain.UpdateStatus = config.UpdatedFailed
}
var result NowcnBaseResult
err = json.Unmarshal(res, &result)
if err != nil {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err.Error())
domain.UpdateStatus = config.UpdatedFailed
}
if result.Error != "" {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, result.Error)
domain.UpdateStatus = config.UpdatedFailed
} else {
util.Log("更新域名解析 %s 成功! IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
}
}
// getRecordList 获取域名记录列表
func (nowcn *Nowcn) getRecordList(domain *config.Domain, typ string) (result NowcnRecordListResp, err error) {
param := map[string]string{
"Domain": domain.DomainName,
"Type": typ,
"Host": domain.GetSubDomain(),
}
res, err := nowcn.request("/api/Dns/DescribeRecordIndex", param, "GET")
err = json.Unmarshal(res, &result)
return
}
func (t *Nowcn) sign(params map[string]string, method string) (string, error) {
// 添加公共参数
params["AccessInstanceID"] = t.DNS.ID
params["SignatureMethod"] = "HMAC-SHA1"
params["SignatureNonce"] = fmt.Sprintf("%d", time.Now().UnixNano())
params["Timestamp"] = time.Now().UTC().Format("2006-01-02T15:04:05Z")
// 1. 排序参数(按首字母顺序)
var keys []string
for k := range params {
if k != "Signature" { // 排除Signature参数
keys = append(keys, k)
}
}
sort.Strings(keys)
// 2. 构造规范化请求字符串
var canonicalizedQuery []string
for _, k := range keys {
// URL编码参数名和参数值
encodedKey := util.PercentEncode(k)
encodedValue := util.PercentEncode(params[k])
canonicalizedQuery = append(canonicalizedQuery, encodedKey+"="+encodedValue)
}
canonicalizedQueryString := strings.Join(canonicalizedQuery, "&")
// 3. 构造待签名字符串
stringToSign := method + "&" + util.PercentEncode("/") + "&" + util.PercentEncode(canonicalizedQueryString)
// 4. 计算HMAC-SHA1签名
key := t.DNS.Secret + "&"
h := hmac.New(sha1.New, []byte(key))
h.Write([]byte(stringToSign))
signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
// 5. 添加签名到参数中
params["Signature"] = signature
// 6. 重新构造最终的查询字符串(包含签名)
keys = append(keys, "Signature")
sort.Strings(keys)
var finalQuery []string
for _, k := range keys {
encodedKey := util.PercentEncode(k)
encodedValue := util.PercentEncode(params[k])
finalQuery = append(finalQuery, encodedKey+"="+encodedValue)
}
return strings.Join(finalQuery, "&"), nil
}
func (t *Nowcn) request(apiPath string, params map[string]string, method string) ([]byte, error) {
// 生成签名
queryString, err := t.sign(params, method)
if err != nil {
return nil, fmt.Errorf("生成签名失败: %v", err)
}
// 构造完整URL
baseURL := "https://api.now.cn"
fullURL := baseURL + apiPath + "?" + queryString
// 创建HTTP请求
req, err := http.NewRequest(method, fullURL, nil)
if err != nil {
return nil, fmt.Errorf("创建请求失败: %v", err)
}
// 设置请求头
req.Header.Set("Accept", "application/json")
// 发送请求
client := t.httpClient
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("请求失败: %v", err)
}
defer resp.Body.Close()
// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应失败: %v", err)
}
// 检查HTTP状态码
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API请求失败,状态码: %d, 响应: %s", resp.StatusCode, string(body))
}
return body, nil
}
================================================
FILE: dns/nsone.go
================================================
package dns
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"github.com/jeessy2/ddns-go/v6/config"
"github.com/jeessy2/ddns-go/v6/util"
)
const nsoneAPIEndpoint = "https://api.nsone.net/v1/zones"
type NSOne struct {
DNS config.DNS
Domains config.Domains
TTL int
httpClient *http.Client
}
type NSOneZone struct {
AssignedNameservers []string `json:"assigned_nameservers"`
DNSServers []string `json:"dns_servers"`
Expiry int `json:"expiry"`
Name string `json:"name"`
Link string `json:"link"`
PrimaryMaster string `json:"primary_master"`
Hostmaster string `json:"hostmaster"`
ID string `json:"id"`
Meta struct {
Asn []string `json:"asn"`
CaProvince []string `json:"ca_province"`
Connections int `json:"connections"`
Country []string `json:"country"`
Georegion []string `json:"georegion"`
HighWatermark float64 `json:"high_watermark"`
IPPrefixes []string `json:"ip_prefixes"`
Latitude float64 `json:"latitude"`
LoadAvg float64 `json:"loadAvg"`
Longitude float64 `json:"longitude"`
LowWatermark float64 `json:"low_watermark"`
Note string `json:"note"`
Priority int `json:"priority"`
Pulsar string `json:"pulsar"`
Requests int `json:"requests"`
Up bool `json:"up"`
UsState []string `json:"us_state"`
Weight float64 `json:"weight"`
} `json:"meta"`
NetworkPools []string `json:"network_pools"`
Networks []int `json:"networks"`
NxTTL int `json:"nx_ttl"`
Serial int `json:"serial"`
Primary struct {
Enabled bool `json:"enabled"`
Secondaries []struct {
IP string `json:"ip"`
Network int `json:"network"`
Notify bool `json:"notify"`
Port int `json:"port"`
Tsig struct {
Enabled bool `json:"enabled"`
Hash string `json:"hash"`
Name string `json:"name"`
Key string `json:"key"`
} `json:"tsig"`
} `json:"secondaries"`
} `json:"primary"`
Refresh int `json:"refresh"`
Retry int `json:"retry"`
Secondary struct {
Status string `json:"status"`
Error string `json:"error"`
LastXfr int `json:"last_xfr"`
LastTry int `json:"last_try"`
Enabled bool `json:"enabled"`
Expired bool `json:"expired"`
PrimaryIP string `json:"primary_ip"`
PrimaryPort int `json:"primary_port"`
PrimaryNetwork int `json:"primary_network"`
Tsig struct {
Enabled bool `json:"enabled"`
Hash string `json:"hash"`
Name string `json:"name"`
Key string `json:"key"`
SignedNotifies bool `json:"signed_notifies"`
} `json:"tsig"`
OtherPorts []int `json:"other_ports"`
OtherIps []string `json:"other_ips"`
OtherNetworks []int `json:"other_networks"`
OtherNotifyOnly []bool `json:"other_notify_only"`
} `json:"secondary"`
TTL int `json:"ttl"`
Zone string `json:"zone"`
Views []string `json:"views"`
LocalTags []string `json:"local_tags"`
Tags struct {
ID int64 `json:"id"`
} `json:"tags"`
CreatedAt int `json:"created_at"`
UpdatedAt int `json:"updated_at"`
Dnssec bool `json:"dnssec"`
Signatures []struct {
Answer []string `json:"answer"`
} `json:"signatures"`
Presigned bool `json:"presigned"`
IDVersion int `json:"id_version"`
ActiveVersion bool `json:"active_version"`
}
type NSOneRecordAnswer struct {
Answer []string `json:"answer"`
ID string `json:"id,omitempty"`
Meta struct {
ID int64 `json:"id,omitempty"`
} `json:"meta,omitempty"`
Region string `json:"region,omitempty"`
Feeds []struct {
Source string `json:"source,omitempty"`
Feed string `json:"feed,omitempty"`
} `json:"feeds,omitempty"`
}
type NSOneRecordResponse struct {
Answers []NSOneRecordAnswer `json:"answers"`
Domain string `json:"domain"`
Filters []struct {
Config struct {
Eliminate bool `json:"eliminate"`
} `json:"config"`
} `json:"filters"`
Link string `json:"link"`
Meta struct {
ID int64 `json:"id"`
} `json:"meta"`
Networks []int `json:"networks"`
Regions struct {
ID int64 `json:"id"`
} `json:"regions"`
Tier int `json:"tier"`
TTL int `json:"ttl"`
OverrideTTL bool `json:"override_ttl"`
Type string `json:"type"`
UseClientSubnet bool `json:"use_client_subnet"`
Zone string `json:"zone"`
ZoneName string `json:"zone_name"`
BlockedTags []string `json:"blocked_tags"`
LocalTags []string `json:"local_tags"`
Tags struct {
ID int64 `json:"id"`
} `json:"tags"`
OverrideAddressRecords bool `json:"override_address_records"`
Signatures []struct {
Answer []string `json:"answer"`
} `json:"signatures"`
CreatedAt int `json:"created_at"`
UpdatedAt int `json:"updated_at"`
ID string `json:"id"`
Customer int `json:"customer"`
Feeds []struct {
Source string `json:"source"`
Feed string `json:"feed"`
} `json:"feeds"`
}
type NSOneRecordRequest struct {
Answers []NSOneRecordAnswer `json:"answers"`
Domain string `json:"domain"`
TTL int `json:"ttl"`
Type string `json:"type"`
Zone string `json:"zone"`
}
func (nsone *NSOne) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpCache, ipv6cache *util.IpCache) {
nsone.Domains.Ipv4Cache = ipv4cache
nsone.Domains.Ipv6Cache = ipv6cache
nsone.DNS = dnsConf.DNS
nsone.Domains.GetNewIp(dnsConf)
if dnsConf.TTL == "" {
nsone.TTL = 60
} else {
ttl, err := strconv.Atoi(dnsConf.TTL)
if err != nil {
// Default TTL in documentation is 1 hour
nsone.TTL = 3600
} else {
nsone.TTL = ttl
}
}
nsone.httpClient = dnsConf.GetHTTPClient()
}
func (nsone *NSOne) AddUpdateDomainRecords() config.Domains {
nsone.addUpdateDomainRecords("A")
nsone.addUpdateDomainRecords("AAAA")
return nsone.Domains
}
func (nsone *NSOne) addUpdateDomainRecords(recordType string) {
ipAddr, domains := nsone.Domains.GetNewIpResult(recordType)
if ipAddr == "" {
return
}
for _, domain := range domains {
zoneInfo, err := nsone.getZone(domain)
if err != nil {
util.Log("查询域名信息发生异常! %s", err)
domain.UpdateStatus = config.UpdatedFailed
continue
}
if zoneInfo == nil {
util.Log("在DNS服务商中未找到根域名: %s", domain.DomainName)
domain.UpdateStatus = config.UpdatedFailed
continue
}
existingRecord, err := nsone.getRecord(domain, recordType)
if err != nil {
util.Log("查询域名信息发生异常! %s", err)
domain.UpdateStatus = config.UpdatedFailed
continue
}
if existingRecord != nil {
nsone.updateRecord(domain, recordType, ipAddr, existingRecord)
} else {
nsone.createRecord(domain, recordType, ipAddr)
}
}
}
func (nsone *NSOne) getZone(domain *config.Domain) (*NSOneZone, error) {
var result NSOneZone
params := url.Values{}
params.Set("records", "false")
err := nsone.request(
"GET",
fmt.Sprintf("%s/%s?%s", nsoneAPIEndpoint, domain.DomainName, params.Encode()),
nil,
&result,
)
if err != nil {
return nil, err
}
return &result, nil
}
func (nsone *NSOne) getRecord(domain *config.Domain, recordType string) (*NSOneRecordResponse, error) {
var result NSOneRecordResponse
params := url.Values{}
params.Set("records", "false")
err := nsone.request(
"GET",
fmt.Sprintf("%s/%s/%s/%s?%s", nsoneAPIEndpoint, domain.DomainName, domain.GetFullDomain(), recordType, params.Encode()),
nil,
&result,
)
if err == nil && len(result.Answers) > 0 {
return &result, nil
}
return nil, nil
}
func (nsone *NSOne) createRecord(domain *config.Domain, recordType string, ipAddr string) {
recordName := domain.GetFullDomain()
request := NSOneRecordRequest{
Answers: []NSOneRecordAnswer{
{
Answer: []string{
ipAddr,
},
},
},
Domain: recordName,
TTL: nsone.TTL,
Type: recordType,
Zone: domain.DomainName,
}
var response NSOneRecordResponse
err := nsone.request(
"PUT",
fmt.Sprintf("%s/%s/%s/%s", nsoneAPIEndpoint, domain.DomainName, recordName, recordType),
request,
&response,
)
if err != nil {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, err)
domain.UpdateStatus = config.UpdatedFailed
return
}
util.Log("新增域名解析 %s 成功! IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
}
func (nsone *NSOne) updateRecord(domain *config.Domain, recordType string, ipAddr string, existingRecord *NSOneRecordResponse) {
if len(existingRecord.Answers) > 0 && len(existingRecord.Answers[0].Answer) > 0 {
if existingRecord.Answers[0].Answer[0] == ipAddr {
util.Log("你的IP %s 没有变化, 域名 %s", ipAddr, domain)
return
}
}
recordName := domain.GetFullDomain()
request := NSOneRecordRequest{
Answers: []NSOneRecordAnswer{
{
Answer: []string{
ipAddr,
},
},
},
Domain: recordName,
TTL: nsone.TTL,
Type: recordType,
Zone: domain.DomainName,
}
var response NSOneRecordResponse
err := nsone.request(
"POST",
fmt.Sprintf("%s/%s/%s/%s", nsoneAPIEndpoint, domain.DomainName, recordName, recordType),
request,
&response,
)
if err != nil {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err)
domain.UpdateStatus = config.UpdatedFailed
return
}
util.Log("更新域名解析 %s 成功! IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
}
func (nsone *NSOne) request(method string, url string, data interface{}, result interface{}) (err error) {
jsonStr := make([]byte, 0)
if data != nil {
jsonStr, _ = json.Marshal(data)
}
req, err := http.NewRequest(
method,
url,
bytes.NewBuffer(jsonStr),
)
if err != nil {
return
}
req.Header.Set("X-NSONE-Key", nsone.DNS.Secret)
req.Header.Set("Content-Type", "application/json")
client := nsone.httpClient
resp, err := client.Do(req)
err = util.GetHTTPResponse(resp, err, result)
return
}
================================================
FILE: dns/porkbun.go
================================================
package dns
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"github.com/jeessy2/ddns-go/v6/config"
"github.com/jeessy2/ddns-go/v6/util"
)
const (
porkbunEndpoint string = "https://api.porkbun.com/api/json/v3/dns"
)
type Porkbun struct {
DNSConfig config.DNS
Domains config.Domains
TTL string
httpClient *http.Client
}
type PorkbunDomainRecord struct {
Name *string `json:"name"` // subdomain
Type *string `json:"type"` // record type, e.g. A AAAA CNAME
Content *string `json:"content"` // value
Ttl *string `json:"ttl"` // default 300
}
type PorkbunResponse struct {
Status string `json:"status"`
}
type PorkbunDomainQueryResponse struct {
*PorkbunResponse
Records []PorkbunDomainRecord `json:"records"`
}
type PorkbunApiKey struct {
AccessKey string `json:"apikey"`
SecretKey string `json:"secretapikey"`
}
type PorkbunDomainCreateOrUpdateVO struct {
*PorkbunApiKey
*PorkbunDomainRecord
}
// Init 初始化
func (pb *Porkbun) Init(conf *config.DnsConfig, ipv4cache *util.IpCache, ipv6cache *util.IpCache) {
pb.Domains.Ipv4Cache = ipv4cache
pb.Domains.Ipv6Cache = ipv6cache
pb.DNSConfig = conf.DNS
pb.Domains.GetNewIp(conf)
if conf.TTL == "" {
// 默认600s
pb.TTL = "600"
} else {
pb.TTL = conf.TTL
}
pb.httpClient = conf.GetHTTPClient()
}
// AddUpdateDomainRecords 添加或更新IPv4/IPv6记录
func (pb *Porkbun) AddUpdateDomainRecords() config.Domains {
pb.addUpdateDomainRecords("A")
pb.addUpdateDomainRecords("AAAA")
return pb.Domains
}
func (pb *Porkbun) addUpdateDomainRecords(recordType string) {
ipAddr, domains := pb.Domains.GetNewIpResult(recordType)
if ipAddr == "" {
return
}
for _, domain := range domains {
var record PorkbunDomainQueryResponse
// 获取当前域名信息
err := pb.request(
porkbunEndpoint+fmt.Sprintf("/retrieveByNameType/%s/%s/%s", domain.DomainName, recordType, domain.SubDomain),
&PorkbunApiKey{
AccessKey: pb.DNSConfig.ID,
SecretKey: pb.DNSConfig.Secret,
},
&record,
)
if err != nil {
util.Log("查询域名信息发生异常! %s", err)
domain.UpdateStatus = config.UpdatedFailed
return
}
if record.Status == "SUCCESS" {
if len(record.Records) > 0 {
// 存在,更新
pb.modify(&record, domain, recordType, ipAddr)
} else {
// 不存在,创建
pb.create(domain, recordType, ipAddr)
}
} else {
util.Log("在DNS服务商中未找到根域名: %s", domain.DomainName)
domain.UpdateStatus = config.UpdatedFailed
}
}
}
// 创建
func (pb *Porkbun) create(domain *config.Domain, recordType string, ipAddr string) {
var response PorkbunResponse
err := pb.request(
porkbunEndpoint+fmt.Sprintf("/create/%s", domain.DomainName),
&PorkbunDomainCreateOrUpdateVO{
PorkbunApiKey: &PorkbunApiKey{
AccessKey: pb.DNSConfig.ID,
SecretKey: pb.DNSConfig.Secret,
},
PorkbunDomainRecord: &PorkbunDomainRecord{
Name: &domain.SubDomain,
Type: &recordType,
Content: &ipAddr,
Ttl: &pb.TTL,
},
},
&response,
)
if err != nil {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, err)
domain.UpdateStatus = config.UpdatedFailed
return
}
if response.Status == "SUCCESS" {
util.Log("新增域名解析 %s 成功! IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
} else {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, response.Status)
domain.UpdateStatus = config.UpdatedFailed
}
}
// 修改
func (pb *Porkbun) modify(record *PorkbunDomainQueryResponse, domain *config.Domain, recordType string, ipAddr string) {
// 相同不修改
if len(record.Records) > 0 && *record.Records[0].Content == ipAddr {
util.Log("你的IP %s 没有变化, 域名 %s", ipAddr, domain)
return
}
var response PorkbunResponse
err := pb.request(
porkbunEndpoint+fmt.Sprintf("/editByNameType/%s/%s/%s", domain.DomainName, recordType, domain.SubDomain),
&PorkbunDomainCreateOrUpdateVO{
PorkbunApiKey: &PorkbunApiKey{
AccessKey: pb.DNSConfig.ID,
SecretKey: pb.DNSConfig.Secret,
},
PorkbunDomainRecord: &PorkbunDomainRecord{
Content: &ipAddr,
Ttl: &pb.TTL,
},
},
&response,
)
if err != nil {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err)
domain.UpdateStatus = config.UpdatedFailed
return
}
if response.Status == "SUCCESS" {
util.Log("更新域名解析 %s 成功! IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
} else {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, response.Status)
domain.UpdateStatus = config.UpdatedFailed
}
}
// request 统一请求接口
func (pb *Porkbun) request(url string, data interface{}, result interface{}) (err error) {
jsonStr := make([]byte, 0)
if data != nil {
jsonStr, _ = json.Marshal(data)
}
req, err := http.NewRequest(
"POST",
url,
bytes.NewBuffer(jsonStr),
)
if err != nil {
return
}
req.Header.Set("Content-Type", "application/json")
client := pb.httpClient
resp, err := client.Do(req)
err = util.GetHTTPResponse(resp, err, result)
return
}
================================================
FILE: dns/rainyun.go
================================================
package dns
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/jeessy2/ddns-go/v6/config"
"github.com/jeessy2/ddns-go/v6/util"
)
const (
rainyunEndpoint = "https://api.v2.rainyun.com"
)
// https://s.apifox.cn/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-153559362
// Rainyun Rainyun
type Rainyun struct {
DNS config.DNS
Domains config.Domains
TTL int
httpClient *http.Client
}
// RainyunRecord 雨云DNS记录
type RainyunRecord struct {
RecordID int64 `json:"record_id"`
Host string `json:"host"`
Type string `json:"type"`
Value string `json:"value"`
Line string `json:"line"`
TTL int `json:"ttl"`
Level int `json:"level"`
}
// RainyunResp 雨云API通用响应
type RainyunResp struct {
Code int `json:"code"`
Message string `json:"message"`
Data any `json:"data"`
}
// Init 初始化
func (rainyun *Rainyun) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpCache, ipv6cache *util.IpCache) {
rainyun.Domains.Ipv4Cache = ipv4cache
rainyun.Domains.Ipv6Cache = ipv6cache
rainyun.DNS = dnsConf.DNS
rainyun.Domains.GetNewIp(dnsConf)
if dnsConf.TTL == "" {
// 默认600s
rainyun.TTL = 600
} else {
ttlInt, _ := strconv.Atoi(dnsConf.TTL)
rainyun.TTL = ttlInt
}
rainyun.httpClient = dnsConf.GetHTTPClient()
}
// AddUpdateDomainRecords 添加或更新IPv4/IPv6记录
func (rainyun *Rainyun) AddUpdateDomainRecords() (domains config.Domains) {
rainyun.addUpdateDomainRecords("A")
rainyun.addUpdateDomainRecords("AAAA")
return rainyun.Domains
}
func (rainyun *Rainyun) addUpdateDomainRecords(recordType string) {
ipAddr, domains := rainyun.Domains.GetNewIpResult(recordType)
if ipAddr == "" {
return
}
for _, domain := range domains {
// 获取Domain ID
domainID := rainyun.DNS.ID
// 获取记录列表
records, err := rainyun.getRecordList(domainID)
if err != nil {
util.Log("查询域名信息发生异常! %s", err)
domain.UpdateStatus = config.UpdatedFailed
continue
}
// 查找匹配的记录
var recordSelected *RainyunRecord
for i := range records {
if strings.EqualFold(records[i].Host, domain.GetSubDomain()) &&
strings.EqualFold(records[i].Type, recordType) {
recordSelected = &records[i]
break
}
}
if recordSelected != nil {
// 更新记录
rainyun.modify(domainID, recordSelected, domain, ipAddr)
} else {
// 新增记录
rainyun.create(domainID, domain, recordType, ipAddr)
}
}
}
// getRecordList 获取域名记录列表
func (rainyun *Rainyun) getRecordList(domainID string) ([]RainyunRecord, error) {
query := url.Values{}
query.Set("limit", "100")
query.Set("page_no", "1")
var result struct {
TotalRecords int `json:"TotalRecords"`
Records []RainyunRecord `json:"Records"`
}
err := rainyun.request(
http.MethodGet,
fmt.Sprintf("/product/domain/%s/dns/", url.PathEscape(domainID)),
query,
nil,
&result,
)
if err != nil {
return nil, err
}
return result.Records, nil
}
// create 创建DNS记录
func (rainyun *Rainyun) create(domainID string, domain *config.Domain, recordType string, ipAddr string) {
record := &RainyunRecord{
Host: domain.GetSubDomain(),
Type: recordType,
Value: ipAddr,
Line: "DEFAULT",
TTL: rainyun.TTL,
Level: 10,
}
err := rainyun.createRecord(domainID, record)
if err != nil {
util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, err)
domain.UpdateStatus = config.UpdatedFailed
return
}
util.Log("新增域名解析 %s 成功! IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
}
// createRecord 发送POST请求创建记录
func (rainyun *Rainyun) createRecord(domainID string, record *RainyunRecord) error {
payload := map[string]any{
"host": record.Host,
"line": record.Line,
"level": record.Level,
"ttl": record.TTL,
"type": record.Type,
"value": record.Value,
"record_id": 0,
}
byt, _ := json.Marshal(payload)
return rainyun.request(
http.MethodPost,
fmt.Sprintf("/product/domain/%s/dns", url.PathEscape(domainID)),
nil,
byt,
nil,
)
}
// modify 修改DNS记录
func (rainyun *Rainyun) modify(domainID string, record *RainyunRecord, domain *config.Domain, ipAddr string) {
if record.Value == ipAddr {
util.Log("你的IP %s 没有变化, 域名 %s", ipAddr, domain)
return
}
record.Value = ipAddr
record.TTL = rainyun.TTL
err := rainyun.patchRecord(domainID, record)
if err != nil {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err)
domain.UpdateStatus = config.UpdatedFailed
return
}
util.Log("更新域名解析 %s 成功! IP: %s", domain, ipAddr)
domain.UpdateStatus = config.UpdatedSuccess
}
// patchRecord 发送PATCH请求更新记录
func (rainyun *Rainyun) patchRecord(domainID string, record *RainyunRecord) error {
payload := map[string]any{
"host": record.Host,
"line": record.Line,
"level": record.Level,
"ttl": record.TTL,
"type": record.Type,
"value": record.Value,
"record_id": record.RecordID,
}
byt, _ := json.Marshal(payload)
return rainyun.request(
http.MethodPatch,
fmt.Sprintf("/product/domain/%s/dns", url.PathEscape(domainID)),
nil,
byt,
nil,
)
}
// request 统一请求接口
func (rainyun *Rainyun) request(method string, path string, query url.Values, body []byte, result any) error {
u, err := url.Parse(rainyunEndpoint)
if err != nil {
return err
}
u.Path = path
if query != nil {
u.RawQuery = query.Encode()
}
var reader *bytes.Reader
if body == nil {
reader = bytes.NewReader(nil)
} else {
reader = bytes.NewReader(body)
}
req, err := http.NewRequest(method, u.String(), reader)
if err != nil {
return err
}
// 认证
req.Header.Set("x-api-key", rainyun.DNS.Secret)
if method == http.MethodPost || method == http.MethodPatch || method == http.MethodPut {
req.Header.Set("Content-Type", "application/json")
}
resp, err := rainyun.httpClient.Do(req)
if err != nil {
return err
}
var apiResp RainyunResp
err = util.GetHTTPResponse(resp, err, &apiResp)
if err != nil {
return err
}
if apiResp.Code != 200 {
if apiResp.Message != "" {
return fmt.Errorf("%s", apiResp.Message)
}
return fmt.Errorf("Rainyun API error, code=%d", apiResp.Code)
}
if result == nil {
return nil
}
dataBytes, err := json.Marshal(apiResp.Data)
if err != nil {
return err
}
return json.Unmarshal(dataBytes, result)
}
================================================
FILE: dns/spaceship.go
================================================
package dns
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"github.com/jeessy2/ddns-go/v6/config"
"github.com/jeessy2/ddns-go/v6/util"
)
const spaceshipAPI = "https://spaceship.dev/api/v1/dns/records"
const maxRecords = 500
type Spaceship struct {
domains config.Domains
header http.Header
ttl int
httpClient *http.Client
}
func (s *Spaceship) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpCache, ipv6cache *util.IpCache) {
s.domains.Ipv4Cache = ipv4cache
s.domains.Ipv6Cache = ipv6cache
s.domains.GetNewIp(dnsConf)
s.ttl = 600
if val, err := strconv.Atoi(dnsConf.TTL); err == nil {
s.ttl = val
}
s.header = http.Header{
"X-API-Key": {dnsConf.DNS.ID},
"X-API-Secret": {dnsConf.DNS.Secret},
"Content-Type": {"application/json"},
}
s.httpClient = dnsConf.GetHTTPClient()
}
func (s *Spaceship) AddUpdateDomainRecords() (domains config.Domains) {
for _, recordType := range []string{"A", "AAAA"} {
ip, domains := s.domains.GetNewIpResult(recordType)
if ip == "" {
continue
}
for _, domain := range domains {
hasUpdated, err := s.updateRecord(recordType, ip, domain)
if err != nil {
util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err)
domain.UpdateStatus = config.UpdatedFailed
continue
}
if !hasUpdated {
util.Log("你的IP %s 没有变化, 域名 %s", ip, domain)
} else {
util.Log("更新域名解析 %s 成功! IP: %s", domain, ip)
domain.UpdateStatus = config.UpdatedSuccess
}
}
}
return s.domains
}
func (s *Spaceship) request(domain *config.Domain, method string, query url.Values, payload []byte) (response []byte, err error) {
url := fmt.Sprintf("%s/%s", spaceshipAPI, domain.DomainName)
req, err := http.NewRequest(method, url, bytes.NewBuffer([]byte(payload)))
if err != nil {
return
}
req.Header = s.header
req.URL.RawQuery = query.Encode()
cli := s.httpClient
resp, err := cli.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
response, err = io.ReadAll(resp.Body)
if err != nil {
return
}
type DataItem struct {
Field string `json:"field"`
Details string `json:"details"`
}
type ErrorResponse struct {
Detail string `json:"detail"`
Data *[]DataItem `json:"data,omitempty"`
}
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
var e ErrorResponse
err = json.Unmarshal(response, &e)
if err != nil {
return
}
err = fmt.Errorf("request error: %s", e.Detail)
return
}
return
}
func (s *Spaceship) createRecord(recordType string, ip string, domain *config.Domain) (err error) {
type Item struct {
Type string `json:"type"`
Address string `json:"address"`
Name string `json:"name"`
TTL int `json:"ttl"`
}
type Payload struct {
Force bool `json:"force"`
Items []Item `json:"items"`
}
payload := Payload{
Force: true,
Items: []Item{
{
Type: recordType,
Address: ip,
Name: domain.SubDomain,
TTL: s.ttl,
},
},
}
data, err := json.Marshal(payload)
if err != nil {
return
}
_, err = s.request(domain, "PUT", url.Values{}, data)
return
}
func (s *Spaceship) getRecords(recordType string, domain *config.Domain) (ips []string, err error) {
type Group struct {
Type string `json:"type"`
}
type Item struct {
Type string `json:"type"`
Address string `json:"address"`
Name string `json:"name"`
TTL int `json:"ttl"`
Group Group `json:"group"`
}
type Response struct {
Items []Item `json:"items"`
Total int `json:"total"`
}
resp, err := s.request(domain, "GET", url.Values{"take": {strconv.Itoa(maxRecords)}, "skip": {"0"}}, []byte{})
if err != nil {
return
}
var response Response
err = json.Unmarshal(resp, &response)
if err != nil {
return
}
if response.Total > maxRecords {
err = fmt.Errorf("could not fetch all %d records in a one request", response.Total)
return
}
for _, item := range response.Items {
if item.Type == recordType && item.Name == domain.SubDomain {
ips = append(ips, item.Address)
}
}
return
}
func (s *Spaceship) deleteRecords(recordType string, domain *config.Domain, ips []string) (err error) {
if len(ips) == 0 {
return
}
if len(ips) > maxRecords {
err = fmt.Errorf("could not delete all %d records in a one request", len(ips))
return
}
type Item struct {
Type string `json:"type"`
Address string `json:"address"`
Name string `json:"name"`
}
var payload []Item
for _, ip := range ips {
payload = append(payload, Item{
Type: recordType,
Address: ip,
Name: domain.SubDomain,
})
}
data, err := json.Marshal(payload)
if err != nil {
return
}
_, err = s.request(domain, "DELETE", url.Values{}, data)
return
}
func (s *Spaceship) updateRecord(recordType string, ip string, domain *config.Domain) (hasUpdated bool, err error) {
ips, err := s.getRecords(recordType, domain)
if err != nil {
return
}
if len(ips) == 1 && ips[0] == ip {
return
}
err = s.deleteRecords(recordType, domain, ips)
if err != nil {
return
}
err = s.createRecord(recordType, ip, domain)
hasUpdated = true
return
}
================================================
FILE: dns/tencent_cloud.go
================================================
package dns
import (
"bytes"
"encoding/json"
"net/http"
"strconv"
"github.com/jeessy2/ddns-go/v6/config"
"github.com/jeessy2/ddns-go/v6/util"
)
const (
tencentCloudEndPoint = "https://dnspod.tencentcloudapi.com"
tencentCloudVersion = "2021-03-23"
)
// TencentCloud 腾讯云 DNSPod API 3.0 实现
// https://cloud.tencent.com/document/api/1427/56193
type TencentCloud struct {
DNS config.DNS
Domains config.Domains
TTL int
httpClient *http.Client
}
// TencentCloudRecord 腾讯云记录
type TencentCloudRecord struct {
Domain string `json:"Domain"`
// DescribeRecordList 不需要 SubDomain
SubDomain string `json:"SubDomain,omitempty"`
// CreateRecord/ModifyRecord 不需要 Subdomain
Subdomain string `json:"Subdomain,omitempty"`
RecordType string `json:"RecordType"`
RecordLine string `json:"RecordLine"`
// DescribeRecordList 不需要 Value
Value string `json:"Value,omitempty"`
// CreateRecord/DescribeRecordList 不需要 RecordId
RecordId int64 `json:"RecordId,omitempty"`
// DescribeRecordList 不需要 TTL
TTL int `json:"TTL,omitempty"`
}
// TencentCloudRecordListsResp 获取域名的解析记录列表返回结果
type TencentCloudRecordListsResp struct {
TencentCloudStatus
Response struct {
RecordCountInfo struct {
TotalCount int `json:"TotalCount"`
} `json:"RecordCountInfo"`
RecordList []TencentCloudRecord `json:"RecordList"`
}
}
// TencentCloudStatus 腾讯云返回状态
// https://cloud.tencent.com/document/product/1427/56192
type TencentCloudStatus struct {
Response struct {
Error struct {
Code string
Message string
}
}
}
func (tc *TencentCloud) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpCache, ipv6cache *util.IpCache) {
tc.Domains.Ipv4Cache = ipv4cache
tc.Domains.Ipv6Cache = ipv6cache
tc.DNS = dnsConf.DNS
tc.Domains.GetNewIp(dnsConf)
if dnsConf.TTL == "" {
// 默认 600s
tc.TTL = 600
} else {
ttl, err := strconv.Atoi(dnsConf.TTL)
if err != nil {
tc.TTL = 600
} else {
tc.TTL = ttl
}
}
tc.httpClient = dnsConf.GetHTTPClient()
}
// AddUpdateDomainRecords 添加或更新 IPv4/IPv6 记录
func (tc *TencentCloud) AddUpdateDomainRecords() config.Domains {
tc.addUpdateDomainRecords("A")
tc.addUpdateDomainRecords("AAAA")
return tc.Domains
}
func (tc *TencentCloud) addUpdateDomainRecords(recordType string) {
ipAddr, domains := tc.Domains.GetNewIpResult(recordType)
if ipAddr == "" {
return
}
for _, domain := range domains {
result, err := tc.getRecordList(domain, recordType)
if err != nil {
util.Log("查询域名信息发生异常! %s", err)
domain.UpdateStatus = config.UpdatedFailed
return
}
if result.Response.RecordCountInfo.TotalCount > 0 {
// 默认第一个
recordSelected := result.Response.RecordList[0]
params := domain.GetCustomParams()
if params.Has("RecordId") {
for i := 0; i < result.Response.RecordCountInfo.TotalCount; i++ {
if strconv.FormatInt(result.Response.RecordList[i].RecordId, 10) == params.Get("RecordId") {
recordSelected = result.Response.RecordList[i]
}
}
}
// 修改记录
tc.modify(recordSelected, domain, recordType, ipAddr)
} else {
// 添加记录
tc.create(domain, recordType, ipAddr)
}
}
}
// create 添加记录
// CreateRecord https://cloud.tencent.com/document/api/1427/56180
func (tc *TencentCloud) create(domain *config.Domain, recordType string, ipAddr string) {
record := &TencentCloudRecord{
Domain: domain.DomainName,
SubDomain: domain.GetSubDo
gitextract_dlov5rlz/
├── .editorconfig
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ ├── config.yml
│ │ └── feature_request.yml
│ ├── dependabot.yml
│ ├── pull_request_template.md
│ └── workflows/
│ ├── dockerhub-description.yml
│ ├── release.yml
│ ├── stale.yml
│ └── test.yml
├── .gitignore
├── .goreleaser.yml
├── .vscode/
│ └── launch.json
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── README_EN.md
├── config/
│ ├── config.go
│ ├── domains.go
│ ├── domains_test.go
│ ├── netInterface.go
│ ├── netInterface_test.go
│ ├── user.go
│ ├── webhook.go
│ └── webhook_test.go
├── dns/
│ ├── alidns.go
│ ├── aliesa.go
│ ├── baidu.go
│ ├── callback.go
│ ├── cloudflare.go
│ ├── dnsla.go
│ ├── dnspod.go
│ ├── dynadot.go
│ ├── dynv6.go
│ ├── edgeone.go
│ ├── eranet.go
│ ├── gcore.go
│ ├── godaddy.go
│ ├── huawei.go
│ ├── index.go
│ ├── name_com.go
│ ├── namecheap.go
│ ├── namesilo.go
│ ├── nowcn.go
│ ├── nsone.go
│ ├── porkbun.go
│ ├── rainyun.go
│ ├── spaceship.go
│ ├── tencent_cloud.go
│ ├── traffic_route.go
│ └── vercel.go
├── go.mod
├── main.go
├── static/
│ ├── common.css
│ ├── constant.js
│ ├── i18n.js
│ ├── theme-button.css
│ ├── theme.js
│ ├── tooltips.js
│ └── utils.js
├── util/
│ ├── aliyun_signer.go
│ ├── aliyun_signer_util.go
│ ├── andriod_time.go
│ ├── baidu_signer.go
│ ├── bcrypt.go
│ ├── copy_url_params.go
│ ├── docker_util.go
│ ├── escape.go
│ ├── http_client_util.go
│ ├── http_util.go
│ ├── huawei_signer.go
│ ├── ip_cache.go
│ ├── messages.go
│ ├── net.go
│ ├── net_resolver.go
│ ├── net_resolver_test.go
│ ├── net_test.go
│ ├── ordinal.go
│ ├── ordinal_test.go
│ ├── osutil/
│ │ ├── daemon_unix.go
│ │ └── daemon_win32.go
│ ├── semver/
│ │ ├── version.go
│ │ └── version_test.go
│ ├── string.go
│ ├── string_test.go
│ ├── tencent_cloud_signer.go
│ ├── termux.go
│ ├── termux_test.go
│ ├── token.go
│ ├── traffic_route_signer.go
│ ├── update/
│ │ ├── apply.go
│ │ ├── apply_test.go
│ │ ├── arch.go
│ │ ├── arm.go
│ │ ├── decompress.go
│ │ ├── decompress_test.go
│ │ ├── detect.go
│ │ ├── errors.go
│ │ ├── latest.go
│ │ ├── package.go
│ │ ├── release.go
│ │ └── update.go
│ ├── user.go
│ └── wait_internet.go
└── web/
├── auth.go
├── login.go
├── login.html
├── logout.go
├── logs.go
├── return_json.go
├── save.go
├── webhookTest.go
├── writing.go
└── writing.html
SYMBOL INDEX (574 symbols across 89 files)
FILE: config/config.go
type DnsConfig (line 28) | type DnsConfig struct
method getIpv4AddrFromInterface (line 230) | func (conf *DnsConfig) getIpv4AddrFromInterface() string {
method getIpv4AddrFromUrl (line 247) | func (conf *DnsConfig) getIpv4AddrFromUrl() string {
method getAddrFromCmd (line 274) | func (conf *DnsConfig) getAddrFromCmd(addrType string) string {
method GetIpv4Addr (line 317) | func (conf *DnsConfig) GetIpv4Addr() string {
method getIpv6AddrFromInterface (line 335) | func (conf *DnsConfig) getIpv6AddrFromInterface() string {
method getIpv6AddrFromUrl (line 379) | func (conf *DnsConfig) getIpv6AddrFromUrl() string {
method GetIpv6Addr (line 408) | func (conf *DnsConfig) GetIpv6Addr() (result string) {
method GetHTTPClient (line 427) | func (conf *DnsConfig) GetHTTPClient() *http.Client {
type DNS (line 56) | type DNS struct
type Config (line 65) | type Config struct
method CompatibleConfig (line 128) | func (conf *Config) CompatibleConfig() {
method SaveConfig (line 168) | func (conf *Config) SaveConfig() (err error) {
method ResetPassword (line 194) | func (conf *Config) ResetPassword(newPassword string) {
method CheckPassword (line 212) | func (conf *Config) CheckPassword(newPassword string) (hashedPwd strin...
type cacheType (line 76) | type cacheType struct
function GetConfigCached (line 85) | func GetConfigCached() (conf Config, err error) {
FILE: config/domains.go
type Domains (line 13) | type Domains struct
method GetNewIp (line 102) | func (domains *Domains) GetNewIp(dnsConf *DnsConfig) {
method GetNewIpResult (line 199) | func (domains *Domains) GetNewIpResult(recordType string) (ipAddr stri...
method GetAllNewIpResult (line 218) | func (domains *Domains) GetAllNewIpResult(multiRecordType string) (res...
type Domain (line 23) | type Domain struct
method String (line 56) | func (d Domain) String() string {
method GetFullDomain (line 64) | func (d Domain) GetFullDomain() string {
method GetSubDomain (line 73) | func (d Domain) GetSubDomain() string {
method GetCustomParams (line 81) | func (d Domain) GetCustomParams() url.Values {
method ToASCII (line 96) | func (d Domain) ToASCII() string {
type DomainTuples (line 33) | type DomainTuples
method append (line 239) | func (domains DomainTuples) append(ipAddr string, retDomains []*Domain...
type DomainTuple (line 36) | type DomainTuple struct
method SetUpdateStatus (line 264) | func (d *DomainTuple) SetUpdateStatus(status updateStatusType) {
method GetIpAddrPool (line 275) | func (d *DomainTuple) GetIpAddrPool(separator string) (result string) {
function checkParseDomains (line 141) | func checkParseDomains(domainArr []string) (domains []*Domain) {
FILE: config/domains_test.go
function TestToASCII (line 8) | func TestToASCII(t *testing.T) {
function TestParseDomainArr (line 54) | func TestParseDomainArr(t *testing.T) {
FILE: config/netInterface.go
type NetInterface (line 9) | type NetInterface struct
function GetNetInterface (line 16) | func GetNetInterface() (ipv4NetInterfaces []NetInterface, ipv6NetInterfa...
FILE: config/netInterface_test.go
function TestGetNetInterface (line 7) | func TestGetNetInterface(t *testing.T) {
FILE: config/user.go
type User (line 4) | type User struct
FILE: config/webhook.go
type Webhook (line 13) | type Webhook struct
type updateStatusType (line 20) | type updateStatusType
constant UpdatedNothing (line 24) | UpdatedNothing updateStatusType = "未改变"
constant UpdatedFailed (line 26) | UpdatedFailed = "失败"
constant UpdatedSuccess (line 28) | UpdatedSuccess = "成功"
function hasJSONPrefix (line 35) | func hasJSONPrefix(s string) bool {
function ExecWebhook (line 40) | func ExecWebhook(domains *Domains, conf *Config) (v4Status updateStatusT...
function getDomainsStatus (line 105) | func getDomainsStatus(domains []*Domain) updateStatusType {
function replacePara (line 125) | func replacePara(domains *Domains, orgPara string, ipv4Result updateStat...
function getDomainsStr (line 137) | func getDomainsStr(domains []*Domain) string {
function extractHeaders (line 152) | func extractHeaders(s string) map[string]string {
FILE: config/webhook_test.go
function TestExtractHeaders (line 9) | func TestExtractHeaders(t *testing.T) {
FILE: dns/alidns.go
constant alidnsEndpoint (line 13) | alidnsEndpoint string = "https://alidns.aliyuncs.com/"
type Alidns (line 18) | type Alidns struct
method Init (line 47) | func (ali *Alidns) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpC...
method AddUpdateDomainRecords (line 62) | func (ali *Alidns) AddUpdateDomainRecords() config.Domains {
method addUpdateDomainRecords (line 68) | func (ali *Alidns) addUpdateDomainRecords(recordType string) {
method create (line 112) | func (ali *Alidns) create(domain *config.Domain, recordType string, ip...
method modify (line 140) | func (ali *Alidns) modify(recordSelected AlidnsRecord, domain *config....
method request (line 175) | func (ali *Alidns) request(params url.Values, result interface{}) (err...
type AlidnsRecord (line 26) | type AlidnsRecord struct
type AlidnsSubDomainRecords (line 33) | type AlidnsSubDomainRecords struct
type AlidnsResp (line 41) | type AlidnsResp struct
FILE: dns/aliesa.go
constant aliesaEndpoint (line 16) | aliesaEndpoint string = "https://esa.cn-hangzhou.aliyuncs.com/"
type Aliesa (line 20) | type Aliesa struct
method Init (line 66) | func (ali *Aliesa) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpC...
method AddUpdateDomainRecords (line 81) | func (ali *Aliesa) AddUpdateDomainRecords() config.Domains {
method addUpdateDomainRecords (line 90) | func (ali *Aliesa) addUpdateDomainRecords(recordType string) {
method create (line 141) | func (ali *Aliesa) create(site AliesaSite, domainTuple *config.DomainT...
method modify (line 182) | func (ali *Aliesa) modify(record AliesaRecord, domainTuple *config.Dom...
method getRecord (line 215) | func (ali *Aliesa) getRecord(site AliesaSite, domainTuple *config.Doma...
method getSite (line 245) | func (ali *Aliesa) getSite(domainTuple *config.DomainTuple) (result Al...
method getOriginPool (line 283) | func (ali *Aliesa) getOriginPool(site AliesaSite, domainTuple *config....
method updateOriginPool (line 314) | func (ali *Aliesa) updateOriginPool(site AliesaSite, domainTuple *conf...
method request (line 374) | func (ali *Aliesa) request(method string, params url.Values, result in...
type AliesaSiteResp (line 31) | type AliesaSiteResp struct
type AliesaSite (line 37) | type AliesaSite struct
type AliesaRecordResp (line 44) | type AliesaRecordResp struct
type AliesaRecord (line 50) | type AliesaRecord struct
type AliesaResp (line 59) | type AliesaResp struct
FILE: dns/baidu.go
constant baiduEndpoint (line 16) | baiduEndpoint = "https://bcd.baidubce.com"
type BaiduCloud (line 19) | type BaiduCloud struct
method Init (line 71) | func (baidu *BaiduCloud) Init(dnsConf *config.DnsConfig, ipv4cache *ut...
method AddUpdateDomainRecords (line 91) | func (baidu *BaiduCloud) AddUpdateDomainRecords() config.Domains {
method addUpdateDomainRecords (line 97) | func (baidu *BaiduCloud) addUpdateDomainRecords(recordType string) {
method create (line 136) | func (baidu *BaiduCloud) create(domain *config.Domain, recordType stri...
method modify (line 157) | func (baidu *BaiduCloud) modify(record BaiduRecord, domain *config.Dom...
method request (line 185) | func (baidu *BaiduCloud) request(method string, url string, data inter...
type BaiduRecord (line 27) | type BaiduRecord struct
type BaiduRecordsResp (line 39) | type BaiduRecordsResp struct
type BaiduListRequest (line 45) | type BaiduListRequest struct
type BaiduModifyRequest (line 52) | type BaiduModifyRequest struct
type BaiduCreateRequest (line 63) | type BaiduCreateRequest struct
FILE: dns/callback.go
type Callback (line 14) | type Callback struct
method Init (line 24) | func (cb *Callback) Init(dnsConf *config.DnsConfig, ipv4cache *util.Ip...
method AddUpdateDomainRecords (line 42) | func (cb *Callback) AddUpdateDomainRecords() config.Domains {
method addUpdateDomainRecords (line 48) | func (cb *Callback) addUpdateDomainRecords(recordType string) {
function replacePara (line 107) | func replacePara(orgPara, ipAddr string, domain *config.Domain, recordTy...
FILE: dns/cloudflare.go
constant zonesAPI (line 16) | zonesAPI = "https://api.cloudflare.com/client/v4/zones"
type Cloudflare (line 19) | type Cloudflare struct
method Init (line 61) | func (cf *Cloudflare) Init(dnsConf *config.DnsConfig, ipv4cache *util....
method AddUpdateDomainRecords (line 81) | func (cf *Cloudflare) AddUpdateDomainRecords() config.Domains {
method addUpdateDomainRecords (line 87) | func (cf *Cloudflare) addUpdateDomainRecords(recordType string) {
method create (line 156) | func (cf *Cloudflare) create(zoneID string, domain *config.Domain, rec...
method modify (line 190) | func (cf *Cloudflare) modify(result CloudflareRecordsResp, zoneID stri...
method getZones (line 228) | func (cf *Cloudflare) getZones(domain *config.Domain) (result Cloudfla...
method request (line 245) | func (cf *Cloudflare) request(method string, url string, data interfac...
type CloudflareZonesResp (line 27) | type CloudflareZonesResp struct
type CloudflareRecordsResp (line 38) | type CloudflareRecordsResp struct
type CloudflareRecord (line 44) | type CloudflareRecord struct
type CloudflareStatus (line 55) | type CloudflareStatus struct
FILE: dns/dnsla.go
constant recordList (line 15) | recordList string = "http://api.dns.la/api/recordList"
constant recordModify (line 16) | recordModify string = "http://api.dns.la/api/record"
constant recordCreate (line 17) | recordCreate string = "http://api.dns.la/api/record"
type Dnsla (line 22) | type Dnsla struct
method Init (line 57) | func (dnsla *Dnsla) Init(dnsConf *config.DnsConfig, ipv4cache *util.Ip...
method AddUpdateDomainRecords (line 73) | func (dnsla *Dnsla) AddUpdateDomainRecords() config.Domains {
method addUpdateDomainRecords (line 79) | func (dnsla *Dnsla) addUpdateDomainRecords(recordType string) {
method create (line 117) | func (dnsla *Dnsla) create(domain *config.Domain, recordType string, i...
method modify (line 159) | func (dnsla *Dnsla) modify(record DnslaRecord, domain *config.Domain, ...
method request (line 208) | func (dnsla *Dnsla) request(method, apiAddr string, values []byte) (bo...
method getRecordList (line 235) | func (dnsla *Dnsla) getRecordList(domain *config.Domain, typ string) (...
type DnslaRecord (line 30) | type DnslaRecord struct
type DnslaRecordListResp (line 38) | type DnslaRecordListResp struct
type DnslaStatus (line 48) | type DnslaStatus struct
FILE: dns/dnspod.go
constant recordListAPI (line 12) | recordListAPI string = "https://dnsapi.cn/Record.List"
constant recordModifyURL (line 13) | recordModifyURL string = "https://dnsapi.cn/Record.Modify"
constant recordCreateAPI (line 14) | recordCreateAPI string = "https://dnsapi.cn/Record.Create"
type Dnspod (line 19) | type Dnspod struct
method Init (line 50) | func (dnspod *Dnspod) Init(dnsConf *config.DnsConfig, ipv4cache *util....
method AddUpdateDomainRecords (line 65) | func (dnspod *Dnspod) AddUpdateDomainRecords() config.Domains {
method addUpdateDomainRecords (line 71) | func (dnspod *Dnspod) addUpdateDomainRecords(recordType string) {
method create (line 107) | func (dnspod *Dnspod) create(domain *config.Domain, recordType string,...
method modify (line 139) | func (dnspod *Dnspod) modify(record DnspodRecord, domain *config.Domai...
method request (line 179) | func (dnspod *Dnspod) request(apiAddr string, values url.Values) (stat...
method getRecordList (line 192) | func (dnspod *Dnspod) getRecordList(domain *config.Domain, typ string)...
type DnspodRecord (line 27) | type DnspodRecord struct
type DnspodRecordListResp (line 36) | type DnspodRecordListResp struct
type DnspodStatus (line 42) | type DnspodStatus struct
FILE: dns/dynadot.go
constant dynadotEndpoint (line 15) | dynadotEndpoint string = "https://www.dynadot.com/set_ddns"
type Dynadot (line 19) | type Dynadot struct
method Init (line 45) | func (dynadot *Dynadot) Init(dnsConf *config.DnsConfig, ipv4cache *uti...
method AddUpdateDomainRecords (line 62) | func (dynadot *Dynadot) AddUpdateDomainRecords() config.Domains {
method addOrUpdateDomainRecords (line 69) | func (dynadot *Dynadot) addOrUpdateDomainRecords(recordType string) {
method createOrModify (line 136) | func (dynadot *Dynadot) createOrModify(record *DynadotRecord, recordTy...
method request (line 170) | func (dynadot *Dynadot) request(params url.Values, result interface{})...
type DynadotRecord (line 29) | type DynadotRecord struct
type DynadotResp (line 38) | type DynadotResp struct
function mergeDomains (line 102) | func mergeDomains(domains []*config.Domain) (records []*DynadotRecord) {
FILE: dns/dynv6.go
constant dynv6Endpoint (line 14) | dynv6Endpoint = "https://dynv6.com"
type Dynv6 (line 17) | type Dynv6 struct
method Init (line 40) | func (dynv6 *Dynv6) Init(dnsConf *config.DnsConfig, ipv4cache *util.Ip...
method AddUpdateDomainRecords (line 55) | func (dynv6 *Dynv6) AddUpdateDomainRecords() config.Domains {
method addUpdateDomainRecords (line 61) | func (dynv6 *Dynv6) addUpdateDomainRecords(recordType string) {
method processSubDomain (line 131) | func (dynv6 *Dynv6) processSubDomain(domain *config.Domain, zone Dynv6...
method findZone (line 145) | func (dynv6 *Dynv6) findZone(domain *config.Domain) (isFind bool, zone...
method findRecord (line 173) | func (dynv6 *Dynv6) findRecord(domain *config.Domain, zoneId string, r...
method modifyMain (line 195) | func (dynv6 *Dynv6) modifyMain(domain *config.Domain, zoneId string, r...
method create (line 215) | func (dynv6 *Dynv6) create(domain *config.Domain, zoneId string, recor...
method modify (line 234) | func (dynv6 *Dynv6) modify(domain *config.Domain, zoneId string, recor...
method request (line 252) | func (dynv6 *Dynv6) request(method string, url string, data interface{...
type Dynv6Zone (line 24) | type Dynv6Zone struct
type Dynv6Record (line 31) | type Dynv6Record struct
FILE: dns/edgeone.go
constant edgeoneEndPoint (line 16) | edgeoneEndPoint = "https://teo.tencentcloudapi.com"
constant edgeoneVersion (line 17) | edgeoneVersion = "2022-09-01"
type EdgeOne (line 20) | type EdgeOne struct
method Init (line 79) | func (eo *EdgeOne) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpC...
method AddUpdateDomainRecords (line 99) | func (eo *EdgeOne) AddUpdateDomainRecords() config.Domains {
method addUpdateDomainRecords (line 105) | func (eo *EdgeOne) addUpdateDomainRecords(recordType string) {
method create (line 155) | func (eo *EdgeOne) create(domain *config.Domain, recordType string, ip...
method modify (line 192) | func (eo *EdgeOne) modify(record EdgeOneRecord, domain *config.Domain,...
method getZone (line 238) | func (eo *EdgeOne) getZone(domain string) (result EdgeOneZoneResponse,...
method getRecordList (line 254) | func (eo *EdgeOne) getRecordList(domain *config.Domain, recordType str...
method getLocation (line 278) | func (eo *EdgeOne) getLocation(domain *config.Domain) string {
method request (line 286) | func (eo *EdgeOne) request(action string, data interface{}, result int...
type EdgeOneRecord (line 27) | type EdgeOneRecord struct
type EdgeOneRecordResponse (line 39) | type EdgeOneRecordResponse struct
type EdgeOneZoneResponse (line 47) | type EdgeOneZoneResponse struct
type Filter (line 58) | type Filter struct
type EdgeOneDescribeDns (line 63) | type EdgeOneDescribeDns struct
type EdgeOneStatus (line 69) | type EdgeOneStatus struct
FILE: dns/eranet.go
type Eranet (line 22) | type Eranet struct
method Init (line 52) | func (eranet *Eranet) Init(dnsConf *config.DnsConfig, ipv4cache *util....
method AddUpdateDomainRecords (line 67) | func (eranet *Eranet) AddUpdateDomainRecords() config.Domains {
method addUpdateDomainRecords (line 73) | func (eranet *Eranet) addUpdateDomainRecords(recordType string) {
method create (line 109) | func (eranet *Eranet) create(domain *config.Domain, recordType string,...
method modify (line 137) | func (eranet *Eranet) modify(record EranetRecord, domain *config.Domai...
method getRecordList (line 172) | func (eranet *Eranet) getRecordList(domain *config.Domain, typ string)...
method queryParams (line 183) | func (eranet *Eranet) queryParams(param map[string]any) string {
method sign (line 197) | func (t *Eranet) sign(params map[string]string, method string) (string...
method request (line 248) | func (t *Eranet) request(apiPath string, params map[string]string, met...
type EranetRecord (line 29) | type EranetRecord struct
type EranetRecordListResp (line 40) | type EranetRecordListResp struct
type EranetBaseResult (line 45) | type EranetBaseResult struct
FILE: dns/gcore.go
constant gcoreAPIEndpoint (line 15) | gcoreAPIEndpoint = "https://api.gcore.com/dns/v2"
type Gcore (line 18) | type Gcore struct
method Init (line 75) | func (gc *Gcore) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpCac...
method AddUpdateDomainRecords (line 95) | func (gc *Gcore) AddUpdateDomainRecords() config.Domains {
method addUpdateDomainRecords (line 101) | func (gc *Gcore) addUpdateDomainRecords(recordType string) {
method getZoneByDomain (line 142) | func (gc *Gcore) getZoneByDomain(domain *config.Domain) (*GcoreZone, e...
method getRRSet (line 166) | func (gc *Gcore) getRRSet(zoneName, recordName, recordType string) (*G...
method createRecord (line 198) | func (gc *Gcore) createRecord(zoneName string, domain *config.Domain, ...
method updateRecord (line 235) | func (gc *Gcore) updateRecord(zoneName string, domain *config.Domain, ...
method request (line 280) | func (gc *Gcore) request(method string, url string, data interface{}, ...
type GcoreZoneResponse (line 26) | type GcoreZoneResponse struct
type GcoreZone (line 32) | type GcoreZone struct
type GcoreRRSetListResponse (line 38) | type GcoreRRSetListResponse struct
type GcoreRRSet (line 44) | type GcoreRRSet struct
type GcoreResourceRecord (line 53) | type GcoreResourceRecord struct
type GcoreInputRRSet (line 61) | type GcoreInputRRSet struct
type GcoreInputResourceRecord (line 68) | type GcoreInputResourceRecord struct
FILE: dns/godaddy.go
type godaddyRecord (line 14) | type godaddyRecord struct
type godaddyRecords (line 21) | type godaddyRecords
type GoDaddyDNS (line 23) | type GoDaddyDNS struct
method Init (line 33) | func (g *GoDaddyDNS) Init(dnsConf *config.DnsConfig, ipv4cache *util.I...
method updateDomainRecord (line 53) | func (g *GoDaddyDNS) updateDomainRecord(recordType string, ipAddr stri...
method AddUpdateDomainRecords (line 88) | func (g *GoDaddyDNS) AddUpdateDomainRecords() config.Domains {
method sendReq (line 98) | func (g *GoDaddyDNS) sendReq(method string, rType string, domain *conf...
FILE: dns/huawei.go
constant huaweicloudEndpoint (line 16) | huaweicloudEndpoint string = "https://dns.myhuaweicloud.com"
type Huaweicloud (line 21) | type Huaweicloud struct
method Init (line 55) | func (hw *Huaweicloud) Init(dnsConf *config.DnsConfig, ipv4cache *util...
method AddUpdateDomainRecords (line 75) | func (hw *Huaweicloud) AddUpdateDomainRecords() config.Domains {
method addUpdateDomainRecords (line 81) | func (hw *Huaweicloud) addUpdateDomainRecords(recordType string) {
method create (line 168) | func (hw *Huaweicloud) create(domain *config.Domain, recordType string...
method modify (line 221) | func (hw *Huaweicloud) modify(record HuaweicloudRecordsets, domain *co...
method getZones (line 260) | func (hw *Huaweicloud) getZones(domain *config.Domain) (result Huaweic...
method request (line 272) | func (hw *Huaweicloud) request(method string, urlString string, data i...
type HuaweicloudZonesResp (line 29) | type HuaweicloudZonesResp struct
type HuaweicloudRecordsResp (line 38) | type HuaweicloudRecordsResp struct
type HuaweicloudRecordsets (line 43) | type HuaweicloudRecordsets struct
FILE: dns/index.go
type DNS (line 11) | type DNS interface
function RunTimer (line 40) | func RunTimer(delay time.Duration) {
function RunOnce (line 48) | func RunOnce() {
FILE: dns/name_com.go
constant listRecords (line 17) | listRecords = "https://api.name.com/core/v1/domains/%s/records"
constant createRecord (line 18) | createRecord = "https://api.name.com/core/v1/domains/%s/records"
constant updateRecord (line 19) | updateRecord = "https://api.name.com/core/v1/domains/%s/records/%d"
type NameCom (line 22) | type NameCom struct
method Init (line 56) | func (n *NameCom) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpCa...
method AddUpdateDomainRecords (line 69) | func (n *NameCom) AddUpdateDomainRecords() (domains config.Domains) {
method addUpdateDomainRecords (line 76) | func (n *NameCom) addUpdateDomainRecords(recordType string) {
method getRecordList (line 115) | func (n *NameCom) getRecordList(domain *config.Domain) (resp *NameComR...
method create (line 121) | func (n *NameCom) create(domain *config.Domain, recordType string, ipA...
method update (line 143) | func (n *NameCom) update(record NameComRecordResp, domain *config.Doma...
method request (line 160) | func (n *NameCom) request(action string, url string, data any, result ...
type NameComRecord (line 29) | type NameComRecord struct
type NameComRecordResp (line 36) | type NameComRecordResp struct
type NameComRecordListResp (line 47) | type NameComRecordListResp struct
FILE: dns/namecheap.go
constant nameCheapEndpoint (line 13) | nameCheapEndpoint string = "https://dynamicdns.park-your-domain.com/upda...
type NameCheap (line 17) | type NameCheap struct
method Init (line 32) | func (nc *NameCheap) Init(dnsConf *config.DnsConfig, ipv4cache *util.I...
method AddUpdateDomainRecords (line 44) | func (nc *NameCheap) AddUpdateDomainRecords() config.Domains {
method addUpdateDomainRecords (line 50) | func (nc *NameCheap) addUpdateDomainRecords(recordType string) {
method modify (line 75) | func (nc *NameCheap) modify(domain *config.Domain, ipAddr string) {
method request (line 96) | func (nc *NameCheap) request(result *NameCheapResp, ipAddr string, dom...
type NameCheapResp (line 26) | type NameCheapResp struct
FILE: dns/namesilo.go
constant nameSiloListRecordEndpoint (line 14) | nameSiloListRecordEndpoint = "https://www.namesilo.com/api/dnsListReco...
constant nameSiloAddRecordEndpoint (line 15) | nameSiloAddRecordEndpoint = "https://www.namesilo.com/api/dnsAddRecor...
constant nameSiloUpdateRecordEndpoint (line 16) | nameSiloUpdateRecordEndpoint = "https://www.namesilo.com/api/dnsUpdateRe...
type NameSilo (line 20) | type NameSilo struct
method Init (line 68) | func (ns *NameSilo) Init(dnsConf *config.DnsConfig, ipv4cache *util.Ip...
method AddUpdateDomainRecords (line 80) | func (ns *NameSilo) AddUpdateDomainRecords() config.Domains {
method addUpdateDomainRecords (line 86) | func (ns *NameSilo) addUpdateDomainRecords(recordType string) {
method modify (line 124) | func (ns *NameSilo) modify(domain *config.Domain, recordID, recordType...
method listRecords (line 151) | func (ns *NameSilo) listRecords(domain *config.Domain) (*NameSiloDNSLi...
method request (line 166) | func (ns *NameSilo) request(ipAddr string, domain *config.Domain, reco...
type NameSiloResp (line 29) | type NameSiloResp struct
type ReplyResponse (line 35) | type ReplyResponse struct
type NameSiloDNSListRecordResp (line 41) | type NameSiloDNSListRecordResp struct
type Request (line 47) | type Request struct
type Reply (line 52) | type Reply struct
type ResourceRecord (line 58) | type ResourceRecord struct
function findResourceRecord (line 197) | func findResourceRecord(data []ResourceRecord, recordType, domain string...
FILE: dns/nowcn.go
type Nowcn (line 22) | type Nowcn struct
method Init (line 55) | func (nowcn *Nowcn) Init(dnsConf *config.DnsConfig, ipv4cache *util.Ip...
method AddUpdateDomainRecords (line 70) | func (nowcn *Nowcn) AddUpdateDomainRecords() config.Domains {
method addUpdateDomainRecords (line 76) | func (nowcn *Nowcn) addUpdateDomainRecords(recordType string) {
method create (line 112) | func (nowcn *Nowcn) create(domain *config.Domain, recordType string, i...
method modify (line 140) | func (nowcn *Nowcn) modify(record NowcnRecord, domain *config.Domain, ...
method getRecordList (line 175) | func (nowcn *Nowcn) getRecordList(domain *config.Domain, typ string) (...
method sign (line 186) | func (t *Nowcn) sign(params map[string]string, method string) (string,...
method request (line 237) | func (t *Nowcn) request(apiPath string, params map[string]string, meth...
type NowcnRecord (line 30) | type NowcnRecord struct
type NowcnRecordListResp (line 42) | type NowcnRecordListResp struct
type NowcnBaseResult (line 48) | type NowcnBaseResult struct
FILE: dns/nsone.go
constant nsoneAPIEndpoint (line 15) | nsoneAPIEndpoint = "https://api.nsone.net/v1/zones"
type NSOne (line 17) | type NSOne struct
method Init (line 177) | func (nsone *NSOne) Init(dnsConf *config.DnsConfig, ipv4cache *util.Ip...
method AddUpdateDomainRecords (line 196) | func (nsone *NSOne) AddUpdateDomainRecords() config.Domains {
method addUpdateDomainRecords (line 202) | func (nsone *NSOne) addUpdateDomainRecords(recordType string) {
method getZone (line 238) | func (nsone *NSOne) getZone(domain *config.Domain) (*NSOneZone, error) {
method getRecord (line 257) | func (nsone *NSOne) getRecord(domain *config.Domain, recordType string...
method createRecord (line 276) | func (nsone *NSOne) createRecord(domain *config.Domain, recordType str...
method updateRecord (line 310) | func (nsone *NSOne) updateRecord(domain *config.Domain, recordType str...
method request (line 351) | func (nsone *NSOne) request(method string, url string, data interface{...
type NSOneZone (line 24) | type NSOneZone struct
type NSOneRecordAnswer (line 114) | type NSOneRecordAnswer struct
type NSOneRecordResponse (line 127) | type NSOneRecordResponse struct
type NSOneRecordRequest (line 169) | type NSOneRecordRequest struct
FILE: dns/porkbun.go
constant porkbunEndpoint (line 14) | porkbunEndpoint string = "https://api.porkbun.com/api/json/v3/dns"
type Porkbun (line 17) | type Porkbun struct
method Init (line 50) | func (pb *Porkbun) Init(conf *config.DnsConfig, ipv4cache *util.IpCach...
method AddUpdateDomainRecords (line 65) | func (pb *Porkbun) AddUpdateDomainRecords() config.Domains {
method addUpdateDomainRecords (line 71) | func (pb *Porkbun) addUpdateDomainRecords(recordType string) {
method create (line 111) | func (pb *Porkbun) create(domain *config.Domain, recordType string, ip...
method modify (line 147) | func (pb *Porkbun) modify(record *PorkbunDomainQueryResponse, domain *...
method request (line 188) | func (pb *Porkbun) request(url string, data interface{}, result interf...
type PorkbunDomainRecord (line 23) | type PorkbunDomainRecord struct
type PorkbunResponse (line 30) | type PorkbunResponse struct
type PorkbunDomainQueryResponse (line 34) | type PorkbunDomainQueryResponse struct
type PorkbunApiKey (line 39) | type PorkbunApiKey struct
type PorkbunDomainCreateOrUpdateVO (line 44) | type PorkbunDomainCreateOrUpdateVO struct
FILE: dns/rainyun.go
constant rainyunEndpoint (line 17) | rainyunEndpoint = "https://api.v2.rainyun.com"
type Rainyun (line 22) | type Rainyun struct
method Init (line 48) | func (rainyun *Rainyun) Init(dnsConf *config.DnsConfig, ipv4cache *uti...
method AddUpdateDomainRecords (line 64) | func (rainyun *Rainyun) AddUpdateDomainRecords() (domains config.Domai...
method addUpdateDomainRecords (line 70) | func (rainyun *Rainyun) addUpdateDomainRecords(recordType string) {
method getRecordList (line 109) | func (rainyun *Rainyun) getRecordList(domainID string) ([]RainyunRecor...
method create (line 132) | func (rainyun *Rainyun) create(domainID string, domain *config.Domain,...
method createRecord (line 154) | func (rainyun *Rainyun) createRecord(domainID string, record *RainyunR...
method modify (line 176) | func (rainyun *Rainyun) modify(domainID string, record *RainyunRecord,...
method patchRecord (line 197) | func (rainyun *Rainyun) patchRecord(domainID string, record *RainyunRe...
method request (line 219) | func (rainyun *Rainyun) request(method string, path string, query url....
type RainyunRecord (line 30) | type RainyunRecord struct
type RainyunResp (line 41) | type RainyunResp struct
FILE: dns/spaceship.go
constant spaceshipAPI (line 16) | spaceshipAPI = "https://spaceship.dev/api/v1/dns/records"
constant maxRecords (line 17) | maxRecords = 500
type Spaceship (line 19) | type Spaceship struct
method Init (line 26) | func (s *Spaceship) Init(dnsConf *config.DnsConfig, ipv4cache *util.Ip...
method AddUpdateDomainRecords (line 43) | func (s *Spaceship) AddUpdateDomainRecords() (domains config.Domains) {
method request (line 67) | func (s *Spaceship) request(domain *config.Domain, method string, quer...
method createRecord (line 111) | func (s *Spaceship) createRecord(recordType string, ip string, domain ...
method getRecords (line 143) | func (s *Spaceship) getRecords(recordType string, domain *config.Domai...
method deleteRecords (line 185) | func (s *Spaceship) deleteRecords(recordType string, domain *config.Do...
method updateRecord (line 216) | func (s *Spaceship) updateRecord(recordType string, ip string, domain ...
FILE: dns/tencent_cloud.go
constant tencentCloudEndPoint (line 14) | tencentCloudEndPoint = "https://dnspod.tencentcloudapi.com"
constant tencentCloudVersion (line 15) | tencentCloudVersion = "2021-03-23"
type TencentCloud (line 20) | type TencentCloud struct
method Init (line 67) | func (tc *TencentCloud) Init(dnsConf *config.DnsConfig, ipv4cache *uti...
method AddUpdateDomainRecords (line 87) | func (tc *TencentCloud) AddUpdateDomainRecords() config.Domains {
method addUpdateDomainRecords (line 93) | func (tc *TencentCloud) addUpdateDomainRecords(recordType string) {
method create (line 131) | func (tc *TencentCloud) create(domain *config.Domain, recordType strin...
method modify (line 165) | func (tc *TencentCloud) modify(record TencentCloudRecord, domain *conf...
method getRecordList (line 201) | func (tc *TencentCloud) getRecordList(domain *config.Domain, recordTyp...
method getRecordLine (line 218) | func (tc *TencentCloud) getRecordLine(domain *config.Domain) string {
method request (line 226) | func (tc *TencentCloud) request(action string, data interface{}, resul...
type TencentCloudRecord (line 28) | type TencentCloudRecord struct
type TencentCloudRecordListsResp (line 45) | type TencentCloudRecordListsResp struct
type TencentCloudStatus (line 58) | type TencentCloudStatus struct
FILE: dns/traffic_route.go
type TrafficRoute (line 13) | type TrafficRoute struct
method Init (line 73) | func (tr *TrafficRoute) Init(dnsConf *config.DnsConfig, ipv4cache *uti...
method AddUpdateDomainRecords (line 92) | func (tr *TrafficRoute) AddUpdateDomainRecords() config.Domains {
method addUpdateDomainRecords (line 98) | func (tr *TrafficRoute) addUpdateDomainRecords(recordType string) {
method getZID (line 140) | func (tr *TrafficRoute) getZID(domain *config.Domain, resp *TrafficRou...
method create (line 170) | func (tr *TrafficRoute) create(zoneID int, domain *config.Domain, reco...
method modify (line 204) | func (tr *TrafficRoute) modify(record TrafficRouteMeta, domain *config...
method parseRequestParams (line 238) | func (tr *TrafficRoute) parseRequestParams(action string, data interfa...
method request (line 277) | func (tr *TrafficRoute) request(method string, action string, data int...
type TrafficRouteMeta (line 21) | type TrafficRouteMeta struct
type TrafficRouteResp (line 32) | type TrafficRouteResp struct
type TrafficRouteListZonesParams (line 64) | type TrafficRouteListZonesParams struct
type TrafficRouteListZonesResp (line 69) | type TrafficRouteListZonesResp struct
FILE: dns/vercel.go
type Vercel (line 15) | type Vercel struct
method Init (line 41) | func (v *Vercel) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpCac...
method AddUpdateDomainRecords (line 59) | func (v *Vercel) AddUpdateDomainRecords() (domains config.Domains) {
method addUpdateDomainRecords (line 65) | func (v *Vercel) addUpdateDomainRecords(recordType string) {
method listExistingRecords (line 119) | func (v *Vercel) listExistingRecords(domain *config.Domain) (records [...
method createRecord (line 129) | func (v *Vercel) createRecord(domain *config.Domain, recordType string...
method updateRecord (line 140) | func (v *Vercel) updateRecord(record *Record, recordType string, recor...
method request (line 149) | func (v *Vercel) request(method, api string, data, result interface{})...
type ListExistingRecordsResponse (line 22) | type ListExistingRecordsResponse struct
type Record (line 26) | type Record struct
FILE: main.go
function main (line 73) | func main() {
function run (line 155) | func run() {
function staticFsFunc (line 184) | func staticFsFunc(writer http.ResponseWriter, request *http.Request) {
function faviconFsFunc (line 188) | func faviconFsFunc(writer http.ResponseWriter, request *http.Request) {
function runWebServer (line 192) | func runWebServer() error {
function runAsDaemon (line 217) | func runAsDaemon() error {
type program (line 248) | type program struct
method Start (line 250) | func (p *program) Start(s service.Service) error {
method run (line 255) | func (p *program) run() {
method Stop (line 258) | func (p *program) Stop(s service.Service) error {
function getService (line 263) | func getService() service.Service {
function uninstallService (line 310) | func uninstallService() {
function installService (line 326) | func installService() {
function restartService (line 354) | func restartService() {
constant sysvScript (line 372) | sysvScript = `#!/bin/sh /etc/rc.common
FILE: static/constant.js
constant DNS_PROVIDERS (line 1) | const DNS_PROVIDERS = {
constant SVG_CODE (line 299) | const SVG_CODE = {
FILE: static/i18n.js
constant I18N_MAP (line 1) | const I18N_MAP = {
constant LANG (line 255) | const LANG = localStorage.getItem('lang') || (navigator.language || navi...
FILE: static/theme.js
function updateColorSchemeMeta (line 1) | function updateColorSchemeMeta(isDark) {
function toggleTheme (line 8) | function toggleTheme(write = false) {
function startPress (line 36) | function startPress() {
function endPress (line 64) | function endPress() {
function cancelPress (line 72) | function cancelPress() {
FILE: static/tooltips.js
class Tooltip (line 1) | class Tooltip {
method constructor (line 2) | constructor(element, triggers) {
method _createTooltipElement (line 9) | _createTooltipElement(options) {
method _updatePosition (line 45) | _updatePosition() {
method show (line 80) | async show(options = {}) {
method hide (line 101) | async hide() {
method _bindEvents (line 117) | _bindEvents(triggers) {
FILE: util/aliyun_signer.go
function HmacSign (line 25) | func HmacSign(signMethod string, httpMethod string, appKeySecret string,...
function HmacSignToB64 (line 38) | func HmacSignToB64(signMethod string, httpMethod string, appKeySecret st...
type strToEnc (line 42) | type strToEnc struct
function makeDataToSign (line 47) | func makeDataToSign(w io.Writer, httpMethod string, vals url.Values) {
function specialUrlEncode (line 67) | func specialUrlEncode(in <-chan *strToEnc, w io.Writer) {
FILE: util/aliyun_signer_util.go
function AliyunSigner (line 10) | func AliyunSigner(accessKeyID, accessSecret string, params *url.Values, ...
FILE: util/andriod_time.go
function FixTimezone (line 9) | func FixTimezone() {
FILE: util/baidu_signer.go
constant BaiduDateFormat (line 16) | BaiduDateFormat = "2006-01-02T15:04:05Z"
constant expirationPeriod (line 17) | expirationPeriod = "1800"
function HmacSha256Hex (line 20) | func HmacSha256Hex(secret, message string) string {
function BaiduCanonicalURI (line 29) | func BaiduCanonicalURI(r *http.Request) string {
function BaiduSigner (line 43) | func BaiduSigner(accessKeyID, accessSecret string, r *http.Request) {
FILE: util/bcrypt.go
function HashPassword (line 8) | func HashPassword(password string) (string, error) {
function PasswordOK (line 17) | func PasswordOK(hashedPassword, password string) bool {
function IsHashedPassword (line 23) | func IsHashedPassword(password string) bool {
FILE: util/copy_url_params.go
function CopyUrlParams (line 5) | func CopyUrlParams(src url.Values, dest url.Values, keys []string) {
FILE: util/docker_util.go
constant DockerEnvFile (line 6) | DockerEnvFile string = "/.dockerenv"
function IsRunInDocker (line 9) | func IsRunInDocker() bool {
FILE: util/escape.go
function shouldEscape (line 8) | func shouldEscape(c byte) bool {
function escape (line 14) | func escape(s string) string {
FILE: util/http_client_util.go
function CreateHTTPClient (line 34) | func CreateHTTPClient() *http.Client {
function GetLocalAddrFromInterface (line 42) | func GetLocalAddrFromInterface(ifaceName string) (string, error) {
function CreateHTTPClientWithInterface (line 60) | func CreateHTTPClientWithInterface(ifaceName string) *http.Client {
function CreateNoProxyHTTPClient (line 128) | func CreateNoProxyHTTPClient(network string) *http.Client {
function SetInsecureSkipVerify (line 143) | func SetInsecureSkipVerify() {
FILE: util/http_util.go
function GetHTTPResponse (line 11) | func GetHTTPResponse(resp *http.Response, err error, result interface{})...
function GetHTTPResponseOrg (line 26) | func GetHTTPResponseOrg(resp *http.Response, err error) ([]byte, error) {
FILE: util/huawei_signer.go
constant BasicDateFormat (line 20) | BasicDateFormat = "20060102T150405Z"
constant Algorithm (line 21) | Algorithm = "SDK-HMAC-SHA256"
constant HeaderXDate (line 22) | HeaderXDate = "X-Sdk-Date"
constant HeaderHost (line 23) | HeaderHost = "host"
constant HeaderAuthorization (line 24) | HeaderAuthorization = "Authorization"
constant HeaderContentSha256 (line 25) | HeaderContentSha256 = "X-Sdk-Content-Sha256"
function hmacsha256 (line 28) | func hmacsha256(key []byte, data string) ([]byte, error) {
function CanonicalRequest (line 46) | func CanonicalRequest(r *http.Request, signedHeaders []string) (string, ...
function CanonicalURI (line 65) | func CanonicalURI(r *http.Request) string {
function CanonicalQueryString (line 79) | func CanonicalQueryString(r *http.Request) string {
function CanonicalHeaders (line 101) | func CanonicalHeaders(r *http.Request, signerHeaders []string) string {
function SignedHeaders (line 121) | func SignedHeaders(r *http.Request) []string {
function RequestPayload (line 131) | func RequestPayload(r *http.Request) ([]byte, error) {
function StringToSign (line 144) | func StringToSign(canonicalRequest string, t time.Time) (string, error) {
function SignStringToSign (line 155) | func SignStringToSign(stringToSign string, signingKey []byte) (string, e...
function HexEncodeSHA256Hash (line 161) | func HexEncodeSHA256Hash(body []byte) (string, error) {
function AuthHeaderValue (line 171) | func AuthHeaderValue(signature, accessKey string, signedHeaders []string...
type Signer (line 176) | type Signer struct
method Sign (line 182) | func (s *Signer) Sign(r *http.Request) error {
FILE: util/ip_cache.go
constant IPCacheTimesENV (line 8) | IPCacheTimesENV = "DDNS_IP_CACHE_TIMES"
type IpCache (line 11) | type IpCache struct
method Check (line 19) | func (d *IpCache) Check(newAddr string) bool {
FILE: util/messages.go
function init (line 14) | func init() {
function Log (line 120) | func Log(key string, args ...interface{}) {
function LogStr (line 124) | func LogStr(key string, args ...interface{}) string {
function InitLogLang (line 128) | func InitLogLang(lang string) string {
FILE: util/net.go
function IsPrivateNetwork (line 11) | func IsPrivateNetwork(remoteAddr string) bool {
function GetRequestIPStr (line 35) | func GetRequestIPStr(r *http.Request) (addr string) {
FILE: util/net_resolver.go
function InitBackupDNS (line 15) | func InitBackupDNS(customDNS, lang string) {
function SetDNS (line 28) | func SetDNS(dns string) {
function LookupHost (line 59) | func LookupHost(url string) error {
FILE: util/net_resolver_test.go
constant testDNS (line 6) | testDNS = "1.1.1.1"
constant testURL (line 7) | testURL = "https://cloudflare.com"
function TestSetDNS (line 10) | func TestSetDNS(t *testing.T) {
function TestLookupHost (line 18) | func TestLookupHost(t *testing.T) {
FILE: util/net_test.go
function TestIsPrivateNetwork (line 9) | func TestIsPrivateNetwork(t *testing.T) {
function TestGetRequestIPStr (line 37) | func TestGetRequestIPStr(t *testing.T) {
FILE: util/ordinal.go
function Ordinal (line 12) | func Ordinal(x int, lang string) string {
FILE: util/ordinal_test.go
function TestOrdinal (line 5) | func TestOrdinal(t *testing.T) {
FILE: util/osutil/daemon_unix.go
function StartDetachedProcess (line 11) | func StartDetachedProcess(exe string, args []string, nullFile *os.File) ...
FILE: util/osutil/daemon_win32.go
function StartDetachedProcess (line 11) | func StartDetachedProcess(exe string, args []string, nullFile *os.File) ...
FILE: util/semver/version.go
constant semVerRegex (line 18) | semVerRegex string = `v?([0-9]+)(\.[0-9]+)?(\.[0-9]+)?` +
type Version (line 23) | type Version struct
method String (line 72) | func (v Version) String() string {
method GreaterThan (line 81) | func (v *Version) GreaterThan(o *Version) bool {
method GreaterThanOrEqual (line 86) | func (v *Version) GreaterThanOrEqual(o *Version) bool {
method compare (line 93) | func (v *Version) compare(o *Version) int {
function init (line 27) | func init() {
function NewVersion (line 34) | func NewVersion(v string) (*Version, error) {
function compareSegment (line 109) | func compareSegment(v, o uint64) int {
FILE: util/semver/version_test.go
function TestNewVersion (line 7) | func TestNewVersion(t *testing.T) {
function TestParts (line 62) | func TestParts(t *testing.T) {
function TestCoerceString (line 79) | func TestCoerceString(t *testing.T) {
function TestCompare (line 105) | func TestCompare(t *testing.T) {
function TestGreaterThan (line 138) | func TestGreaterThan(t *testing.T) {
function TestGreaterThanOrEqual (line 174) | func TestGreaterThanOrEqual(t *testing.T) {
FILE: util/string.go
function WriteString (line 9) | func WriteString(strs ...string) string {
function toHostname (line 23) | func toHostname(url string) string {
function SplitLines (line 31) | func SplitLines(s string) []string {
function PercentEncode (line 39) | func PercentEncode(value string) string {
FILE: util/string_test.go
function TestWriteString (line 5) | func TestWriteString(t *testing.T) {
function TestToHostname (line 24) | func TestToHostname(t *testing.T) {
FILE: util/tencent_cloud_signer.go
function sha256hex (line 13) | func sha256hex(s string) string {
function tencentCloudHmacsha256 (line 18) | func tencentCloudHmacsha256(s, key string) string {
constant DnsPod (line 25) | DnsPod = "dnspod"
constant EdgeOne (line 26) | EdgeOne = "teo"
function TencentCloudSigner (line 30) | func TencentCloudSigner(secretId string, secretKey string, r *http.Reque...
FILE: util/termux.go
function isTermux (line 8) | func isTermux() bool {
FILE: util/termux_test.go
function TestIsTermux (line 9) | func TestIsTermux(t *testing.T) {
FILE: util/token.go
function GenerateToken (line 13) | func GenerateToken(username string) string {
function generateRandomKey (line 22) | func generateRandomKey() string {
FILE: util/traffic_route_signer.go
constant Version (line 15) | Version = "2018-08-01"
constant Service (line 16) | Service = "DNS"
constant Region (line 17) | Region = "cn-north-1"
constant Host (line 18) | Host = "open.volcengineapi.com"
function hmacSHA256 (line 22) | func hmacSHA256(key []byte, content string) []byte {
function hashSHA256 (line 29) | func hashSHA256(content []byte) string {
type RequestParam (line 37) | type RequestParam struct
type Credentials (line 47) | type Credentials struct
type SignRequest (line 55) | type SignRequest struct
function TrafficRouteSigner (line 64) | func TrafficRouteSigner(method string, query map[string][]string, header...
FILE: util/update/apply.go
function apply (line 33) | func apply(update io.Reader, targetPath string) error {
FILE: util/update/apply_test.go
function cleanup (line 17) | func cleanup(path string) {
function writeOldFile (line 23) | func writeOldFile(path string, t *testing.T) {
function validateUpdate (line 32) | func validateUpdate(path string, err error, t *testing.T) {
function TestApply (line 47) | func TestApply(t *testing.T) {
FILE: util/update/arch.go
constant minARM (line 11) | minARM = 5
constant maxARM (line 12) | maxARM = 7
function generateAdditionalArch (line 16) | func generateAdditionalArch() []string {
FILE: util/update/decompress.go
function decompressCommand (line 30) | func decompressCommand(src io.Reader, url, cmd string) (io.Reader, error) {
function unzip (line 40) | func unzip(src io.Reader, cmd string) (io.Reader, error) {
function untar (line 64) | func untar(src io.Reader, cmd string) (io.Reader, error) {
function matchExecutableName (line 87) | func matchExecutableName(cmd, target string) bool {
FILE: util/update/decompress_test.go
function TestCompressionNotRequired (line 14) | func TestCompressionNotRequired(t *testing.T) {
function TestMatchExecutableName (line 31) | func TestMatchExecutableName(t *testing.T) {
function TestErrorFromReader (line 53) | func TestErrorFromReader(t *testing.T) {
FILE: util/update/detect.go
function detectLatest (line 15) | func detectLatest(repo string) (latest *Latest, found bool, err error) {
function findAsset (line 30) | func findAsset(rel *Release) (*Asset, *semver.Version, bool) {
function findAssetForArch (line 43) | func findAssetForArch(arch string, rel *Release,
function findAssetFromRelease (line 63) | func findAssetFromRelease(rel *Release, suffixes []string) (*Asset, *sem...
function assetMatchSuffixes (line 86) | func assetMatchSuffixes(name string, suffixes []string) bool {
function getSuffixes (line 99) | func getSuffixes(arch string) []string {
FILE: util/update/latest.go
type Latest (line 8) | type Latest struct
function newLatest (line 17) | func newLatest(asset *Asset, ver *semver.Version) *Latest {
FILE: util/update/package.go
function Self (line 15) | func Self(version string) {
function to (line 55) | func to(assetURL, assetFileName, cmdPath string) error {
function downloadAssetFromURL (line 64) | func downloadAssetFromURL(url string) (rc io.ReadCloser, err error) {
FILE: util/update/release.go
type Release (line 12) | type Release struct
type Asset (line 17) | type Asset struct
type ReleaseResp (line 23) | type ReleaseResp struct
function getLatest (line 34) | func getLatest(repo string) (*Release, error) {
function newRelease (line 53) | func newRelease(from *ReleaseResp) *Release {
FILE: util/update/update.go
function decompressAndUpdate (line 10) | func decompressAndUpdate(src io.Reader, assetName, cmdPath string) error {
FILE: util/user.go
constant ConfigFilePathENV (line 7) | ConfigFilePathENV = "DDNS_CONFIG_FILE_PATH"
function GetConfigFilePath (line 10) | func GetConfigFilePath() string {
function GetConfigFilePathDefault (line 19) | func GetConfigFilePathDefault() string {
FILE: util/wait_internet.go
function WaitInternet (line 14) | func WaitInternet(addresses []string) {
function isDNSErr (line 48) | func isDNSErr(e error) bool {
FILE: web/auth.go
type ViewFunc (line 12) | type ViewFunc
function Auth (line 15) | func Auth(f ViewFunc) ViewFunc {
function AuthAssert (line 47) | func AuthAssert(f ViewFunc) ViewFunc {
FILE: web/login.go
constant cookieName (line 20) | cookieName = "token"
constant saveLimit (line 29) | saveLimit = time.Duration(30) * time.Minute
constant loginFailLockDuration (line 32) | loginFailLockDuration = time.Duration(30) * time.Minute
type loginDetect (line 35) | type loginDetect struct
function Login (line 43) | func Login(writer http.ResponseWriter, request *http.Request) {
function LoginFunc (line 65) | func LoginFunc(w http.ResponseWriter, r *http.Request) {
function loginUnlock (line 154) | func loginUnlock() {
FILE: web/logout.go
function Logout (line 8) | func Logout(w http.ResponseWriter, r *http.Request) {
FILE: web/logs.go
type MemoryLogs (line 12) | type MemoryLogs struct
method Write (line 17) | func (mlogs *MemoryLogs) Write(p []byte) (n int, err error) {
function init (line 29) | func init() {
function Logs (line 35) | func Logs(writer http.ResponseWriter, request *http.Request) {
function ClearLog (line 42) | func ClearLog(writer http.ResponseWriter, request *http.Request) {
FILE: web/return_json.go
type Result (line 9) | type Result struct
function returnError (line 16) | func returnError(w http.ResponseWriter, msg string) {
function returnOK (line 26) | func returnOK(w http.ResponseWriter, msg string, data interface{}) {
FILE: web/save.go
function Save (line 14) | func Save(writer http.ResponseWriter, request *http.Request) {
function checkAndSave (line 26) | func checkAndSave(request *http.Request) string {
FILE: web/webhookTest.go
function WebhookTest (line 11) | func WebhookTest(writer http.ResponseWriter, request *http.Request) {
FILE: web/writing.go
constant VersionEnv (line 18) | VersionEnv = "DDNS_GO_VERSION"
type dnsConf4JS (line 21) | type dnsConf4JS struct
function Writing (line 45) | func Writing(writer http.ResponseWriter, request *http.Request) {
function getDnsConfStr (line 99) | func getDnsConfStr(dnsConf []config.DnsConfig) string {
constant displayCount (line 132) | displayCount int = 3
function getHideIDSecret (line 135) | func getHideIDSecret(conf *config.DnsConfig) (idHide string, secretHide ...
Condensed preview — 115 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (420K chars).
[
{
"path": ".editorconfig",
"chars": 375,
"preview": "# http://editorconfig.org\n\nroot = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\ntrim_trailing_w"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.yml",
"chars": 2065,
"preview": "name: Bug\ndescription: Report a bug in ddns-go\nlabels: ['bug']\n\nbody:\n - type: textarea\n attributes:\n label: De"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 206,
"preview": "blank_issues_enabled: false\ncontact_links:\n - name: None of the above?\n url: https://github.com/jeessy2/ddns-go/disc"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.yml",
"chars": 940,
"preview": "name: Feature Request\ndescription: Feature request for ddns-go\nlabels: ['enhancement']\n\nbody:\n - type: textarea\n att"
},
{
"path": ".github/dependabot.yml",
"chars": 587,
"preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
},
{
"path": ".github/pull_request_template.md",
"chars": 58,
"preview": "# What does this PR do?\n\n# Motivation\n\n# Additional Notes\n"
},
{
"path": ".github/workflows/dockerhub-description.yml",
"chars": 651,
"preview": "name: Update Docker Hub Description\non:\n push:\n branches:\n - master\n paths:\n - README.md\n - .githu"
},
{
"path": ".github/workflows/release.yml",
"chars": 1419,
"preview": "name: release\n\non:\n push:\n # Sequence of patterns matched against refs/tags\n tags:\n - 'v*' # Push events to "
},
{
"path": ".github/workflows/stale.yml",
"chars": 1080,
"preview": "name: Close stale issues and PRs\n\non:\n schedule:\n - cron: \"30 1 * * *\"\n\njobs:\n stale:\n permissions:\n issues: "
},
{
"path": ".github/workflows/test.yml",
"chars": 755,
"preview": "name: test\n\non:\n push:\n pull_request:\n\njobs:\n test:\n runs-on: ubuntu-latest\n strategy:\n matrix:\n go"
},
{
"path": ".gitignore",
"chars": 326,
"preview": "# Compiled Object files, Static and Dynamic libs (Shared Objects)\n*.o\n*.a\n*.so\n/ddns-go\n__*\n\n# Folders\n_obj\n_test\n.vagra"
},
{
"path": ".goreleaser.yml",
"chars": 5384,
"preview": "# This is an example goreleaser.yaml file with some sane defaults.\n# Make sure to check the documentation at http://gore"
},
{
"path": ".vscode/launch.json",
"chars": 494,
"preview": "{\n // Use IntelliSense to learn about possible attributes.\n // Hover to view descriptions of existing attributes.\n"
},
{
"path": "Dockerfile",
"chars": 270,
"preview": "FROM alpine\nLABEL name=ddns-go\nLABEL url=https://github.com/jeessy2/ddns-go\nRUN apk add --no-cache curl grep\n\nWORKDIR /a"
},
{
"path": "LICENSE",
"chars": 1062,
"preview": "MIT License\n\nCopyright (c) 2020 jeessy\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof "
},
{
"path": "Makefile",
"chars": 855,
"preview": ".PHONY: build clean test test-race\n\n# 如果找不到 tag 则使用 HEAD commit\nVERSION=$(shell git describe --tags `git rev-list --tags"
},
{
"path": "README.md",
"chars": 7544,
"preview": "# DDNS-GO\n\n[ !"
},
{
"path": "README_EN.md",
"chars": 6829,
"preview": "# DDNS-GO\n\n[ !"
},
{
"path": "config/config.go",
"chars": 10420,
"preview": "package config\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n"
},
{
"path": "config/domains.go",
"chars": 7328,
"preview": "package config\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/jeessy2/ddns-go/v6/util\"\n\t\"golang.org/x/net/idna\"\n\t\"golang."
},
{
"path": "config/domains_test.go",
"chars": 2876,
"preview": "package config\n\nimport \"testing\"\n\n// TestToASCII test converts the name of [Domain] to its ASCII form.\n//\n// Copied from"
},
{
"path": "config/netInterface.go",
"chars": 1526,
"preview": "package config\n\nimport (\n\t\"fmt\"\n\t\"net\"\n)\n\n// NetInterface 本机网络\ntype NetInterface struct {\n\tName string\n\tAddress []str"
},
{
"path": "config/netInterface_test.go",
"chars": 226,
"preview": "package config\n\nimport (\n\t\"testing\"\n)\n\nfunc TestGetNetInterface(t *testing.T) {\n\tipv4NetInterfaces, ipv6NetInterfaces, e"
},
{
"path": "config/user.go",
"chars": 84,
"preview": "package config\n\n// User 登录用户\ntype User struct {\n\tUsername string\n\tPassword string\n}\n"
},
{
"path": "config/webhook.go",
"chars": 4215,
"preview": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/jeessy2/ddns-go/v6/util\"\n)\n\n//"
},
{
"path": "config/webhook_test.go",
"chars": 375,
"preview": "package config\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n)\n\n// TestExtractHeaders 测试 parseHeaderArr\nfunc TestExtractHeaders(t *tes"
},
{
"path": "dns/alidns.go",
"chars": 4604,
"preview": "package dns\n\nimport (\n\t\"bytes\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/jeessy2/ddns-go/v6/config\"\n\t\"github.com/jeessy2/ddns"
},
{
"path": "dns/aliesa.go",
"chars": 10030,
"preview": "package dns\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/jeessy2/ddns-"
},
{
"path": "dns/baidu.go",
"chars": 5013,
"preview": "package dns\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"strconv\"\n\n\t\"github.com/jeessy2/ddns-go/v6/config\"\n\t\"github"
},
{
"path": "dns/callback.go",
"chars": 3111,
"preview": "package dns\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/jeessy2/ddns-go/v6/config\""
},
{
"path": "dns/cloudflare.go",
"chars": 6021,
"preview": "package dns\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/jeessy"
},
{
"path": "dns/dnsla.go",
"chars": 6653,
"preview": "package dns\n\nimport (\n\t\"bytes\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"github.com/jeessy2/ddns-go/v6/config\"\n\t\"github.com/"
},
{
"path": "dns/dnspod.go",
"chars": 5164,
"preview": "package dns\n\nimport (\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/jeessy2/ddns-go/v6/config\"\n\t\"github.com/jeessy2/ddns-go/v6/ut"
},
{
"path": "dns/dynadot.go",
"chars": 4480,
"preview": "package dns\n\nimport (\n\t\"bytes\"\n\t\"github.com/jeessy2/ddns-go/v6/config\"\n\t\"github.com/jeessy2/ddns-go/v6/util\"\n\t\"net/http\""
},
{
"path": "dns/dynv6.go",
"chars": 6655,
"preview": "package dns\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"github.com/jeessy2/ddns-go/v6/config\"\n\t\"github.com/jeessy2/ddns-go/v6/"
},
{
"path": "dns/edgeone.go",
"chars": 7740,
"preview": "package dns\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"strconv\"\n\n\t\"github.com/jeessy2/ddns-go/v6/config\"\n\t\"github"
},
{
"path": "dns/eranet.go",
"chars": 7358,
"preview": "package dns\n\nimport (\n\t\"crypto/hmac\"\n\t\"crypto/sha1\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/u"
},
{
"path": "dns/gcore.go",
"chars": 7008,
"preview": "package dns\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\n\t\"github.com/jeessy2/ddns-go/v"
},
{
"path": "dns/godaddy.go",
"chars": 2825,
"preview": "package dns\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\n\t\"github.com/jeessy2/ddns-go/v6/config\"\n\t"
},
{
"path": "dns/huawei.go",
"chars": 7045,
"preview": "package dns\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\n\t\"github.com/jeessy2/ddns-go/v"
},
{
"path": "dns/index.go",
"chars": 2746,
"preview": "package dns\n\nimport (\n\t\"time\"\n\n\t\"github.com/jeessy2/ddns-go/v6/config\"\n\t\"github.com/jeessy2/ddns-go/v6/util\"\n)\n\n// DNS i"
},
{
"path": "dns/name_com.go",
"chars": 4670,
"preview": "package dns\n\nimport (\n\t\"bytes\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.co"
},
{
"path": "dns/namecheap.go",
"chars": 2888,
"preview": "package dns\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/jeessy2/ddns-go/v6/config\"\n\t\"github.com/jeessy2/ddns-go"
},
{
"path": "dns/namesilo.go",
"chars": 5190,
"preview": "package dns\n\nimport (\n\t\"encoding/xml\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/jeessy2/ddns-go/v6/config\"\n\t\"github.com"
},
{
"path": "dns/nowcn.go",
"chars": 6920,
"preview": "package dns\n\nimport (\n\t\"crypto/hmac\"\n\t\"crypto/sha1\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"sort\""
},
{
"path": "dns/nsone.go",
"chars": 10097,
"preview": "package dns\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\n\t\"github.com/jeessy2/ddns-go/v"
},
{
"path": "dns/porkbun.go",
"chars": 4863,
"preview": "package dns\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/jeessy2/ddns-go/v6/config\"\n\t\"github.com"
},
{
"path": "dns/rainyun.go",
"chars": 6231,
"preview": "package dns\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/jeessy"
},
{
"path": "dns/spaceship.go",
"chars": 5135,
"preview": "package dns\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\n\t\"github.com/jeessy2/ddn"
},
{
"path": "dns/tencent_cloud.go",
"chars": 6314,
"preview": "package dns\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"strconv\"\n\n\t\"github.com/jeessy2/ddns-go/v6/config\"\n\t\"github"
},
{
"path": "dns/traffic_route.go",
"chars": 7068,
"preview": "package dns\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"strconv\"\n\n\t\"github.com/jeessy2/ddns-go/v6/config\"\n\t\"github.com/jees"
},
{
"path": "dns/vercel.go",
"chars": 4448,
"preview": "package dns\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/jeessy2/ddns-go/v"
},
{
"path": "go.mod",
"chars": 306,
"preview": "module github.com/jeessy2/ddns-go/v6\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/kardianos/service v1.2.4\n\tgithub.com/wagslane/go-"
},
{
"path": "main.go",
"chars": 9707,
"preview": "package main\n\nimport (\n\t\"embed\"\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"r"
},
{
"path": "static/common.css",
"chars": 6519,
"preview": ":root {\n color-scheme: light;\n --bg-color: #f2f3f8;\n --text-color: black;\n}\n\n[data-theme=\"dark\"] {\n color-sc"
},
{
"path": "static/constant.js",
"chars": 12091,
"preview": "const DNS_PROVIDERS = {\n alidns: {\n name: {\n \"en\": \"Aliyun\",\n \"zh-cn\": \"阿里云\",\n },\n idLabel: \"AccessK"
},
{
"path": "static/i18n.js",
"chars": 8762,
"preview": "const I18N_MAP = {\n 'Logs': {\n 'en': 'Logs',\n 'zh-cn': '日志'\n },\n 'Save': {\n 'en': 'Save',\n 'zh-cn': '保存'\n"
},
{
"path": "static/theme-button.css",
"chars": 788,
"preview": "/* From https://css.gg */\n\n.gg-dark-mode {\n box-sizing: border-box;\n position: relative;\n display: block;\n t"
},
{
"path": "static/theme.js",
"chars": 2545,
"preview": "function updateColorSchemeMeta(isDark) {\n const meta = document.querySelector('meta[name=\"color-scheme\"]');\n if (meta)"
},
{
"path": "static/tooltips.js",
"chars": 4925,
"preview": "class Tooltip {\n constructor(element, triggers) {\n this.$element = element;\n this.$tooltip = null;\n this.origi"
},
{
"path": "static/utils.js",
"chars": 2927,
"preview": "const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))\n\nconst html2Element = (htmlString) => {\n const "
},
{
"path": "util/aliyun_signer.go",
"chars": 1870,
"preview": "package util\n\nimport (\n\t\"crypto/hmac\"\n\t\"crypto/md5\"\n\t\"crypto/sha1\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"hash\"\n\t\""
},
{
"path": "util/aliyun_signer_util.go",
"chars": 634,
"preview": "package util\n\nimport (\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n)\n\n// AliyunSigner AliyunSigner\nfunc AliyunSigner(accessKeyID, acce"
},
{
"path": "util/andriod_time.go",
"chars": 310,
"preview": "package util\n\nimport (\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n)\n\nfunc FixTimezone() {\n\tout, err := exec.Command(\"/system/bin/getp"
},
{
"path": "util/baidu_signer.go",
"chars": 1676,
"preview": "package util\n\nimport (\n\t\"crypto/hmac\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n)\n\n// https"
},
{
"path": "util/bcrypt.go",
"chars": 609,
"preview": "package util\n\nimport (\n\t\"golang.org/x/crypto/bcrypt\"\n)\n\n// HashPassword 密码哈希\nfunc HashPassword(password string) (string,"
},
{
"path": "util/copy_url_params.go",
"chars": 314,
"preview": "package util\n\nimport \"net/url\"\n\nfunc CopyUrlParams(src url.Values, dest url.Values, keys []string) {\n\tif keys == nil || "
},
{
"path": "util/docker_util.go",
"chars": 216,
"preview": "package util\n\nimport \"os\"\n\n// DockerEnvFile Docker容器中包含的文件\nconst DockerEnvFile string = \"/.dockerenv\"\n\n// IsRunInDocker "
},
{
"path": "util/escape.go",
"chars": 885,
"preview": "// based on https://github.com/golang/go/blob/master/src/net/url/url.go\n// Copyright 2009 The Go Authors. All rights res"
},
{
"path": "util/http_client_util.go",
"chars": 4087,
"preview": "package util\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"time\"\n)\n\nvar dialer = &net.Dialer{\n\tTimeout:"
},
{
"path": "util/http_util.go",
"chars": 811,
"preview": "package util\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n)\n\n// GetHTTPResponse 处理HTTP结果,返回序列化的json\nfunc GetHTTPR"
},
{
"path": "util/huawei_signer.go",
"chars": 5214,
"preview": "// HWS API Gateway Signature\n// based on https://github.com/datastream/aws/blob/master/signv4.go\n// Copyright (c) 2014, "
},
{
"path": "util/ip_cache.go",
"chars": 617,
"preview": "package util\n\nimport (\n\t\"os\"\n\t\"strconv\"\n)\n\nconst IPCacheTimesENV = \"DDNS_IP_CACHE_TIMES\"\n\n// IpCache 上次IP缓存\ntype IpCache"
},
{
"path": "util/messages.go",
"chars": 9444,
"preview": "package util\n\nimport (\n\t\"log\"\n\t\"strings\"\n\n\t\"golang.org/x/text/language\"\n\t\"golang.org/x/text/message\"\n)\n\nvar logLang = la"
},
{
"path": "util/net.go",
"chars": 1118,
"preview": "package util\n\nimport (\n\t\"net\"\n\t\"net/http\"\n\t\"strings\"\n)\n\n// IsPrivateNetwork 是否为私有地址\n// https://en.wikipedia.org/wiki/Pri"
},
{
"path": "util/net_resolver.go",
"chars": 1306,
"preview": "package util\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"golang.org/x/text/language\"\n)\n\n// BackupDNS will be us"
},
{
"path": "util/net_resolver_test.go",
"chars": 714,
"preview": "package util\n\nimport \"testing\"\n\nconst (\n\ttestDNS = \"1.1.1.1\"\n\ttestURL = \"https://cloudflare.com\"\n)\n\nfunc TestSetDNS(t *t"
},
{
"path": "util/net_test.go",
"chars": 1086,
"preview": "package util\n\nimport (\n\t\"net/http\"\n\t\"testing\"\n)\n\n// TestIsPrivateNetwork 测试是否为私有地址\nfunc TestIsPrivateNetwork(t *testing."
},
{
"path": "util/ordinal.go",
"chars": 568,
"preview": "package util\n\nimport (\n\t\"strconv\"\n\n\t\"golang.org/x/text/language\"\n)\n\n// Ordinal returns the ordinal format of the given n"
},
{
"path": "util/ordinal_test.go",
"chars": 954,
"preview": "package util\n\nimport \"testing\"\n\nfunc TestOrdinal(t *testing.T) {\n\tlang := \"en\"\n\n\ttests := []struct {\n\t\tname string\n\t\tgot"
},
{
"path": "util/osutil/daemon_unix.go",
"chars": 463,
"preview": "//go:build !windows\n\npackage osutil\n\nimport (\n\t\"os\"\n\t\"syscall\"\n)\n\n// StartDetachedProcess starts a process detached from"
},
{
"path": "util/osutil/daemon_win32.go",
"chars": 668,
"preview": "//go:build windows\n\npackage osutil\n\nimport (\n\t\"os\"\n\t\"syscall\"\n)\n\n// StartDetachedProcess starts a process detached from "
},
{
"path": "util/semver/version.go",
"chars": 2369,
"preview": "// Based on https://github.com/Masterminds/semver/blob/v3.2.1/version.go\n\npackage semver\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"reg"
},
{
"path": "util/semver/version_test.go",
"chars": 4432,
"preview": "// Based on https://github.com/Masterminds/semver/blob/v3.2.1/version_test.go\n\npackage semver\n\nimport \"testing\"\n\nfunc Te"
},
{
"path": "util/string.go",
"chars": 1086,
"preview": "package util\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n)\n\n// WriteString creates a new string using [strings.Builder].\nfunc WriteS"
},
{
"path": "util/string_test.go",
"chars": 995,
"preview": "package util\n\nimport \"testing\"\n\nfunc TestWriteString(t *testing.T) {\n\ttests := []struct {\n\t\tinput []string\n\t\texpected"
},
{
"path": "util/tencent_cloud_signer.go",
"chars": 2185,
"preview": "package util\n\nimport (\n\t\"crypto/hmac\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\nfunc"
},
{
"path": "util/termux.go",
"chars": 195,
"preview": "package util\n\nimport \"os\"\n\n// isTermux 是否在 Termux 中运行\n//\n// https://wiki.termux.com/wiki/Getting_started\nfunc isTermux()"
},
{
"path": "util/termux_test.go",
"chars": 382,
"preview": "package util\n\nimport (\n\t\"os\"\n\t\"testing\"\n)\n\n// TestIsTermux 测试在或不在 Termux 中运行都能正确判断\nfunc TestIsTermux(t *testing.T) {\n\t//"
},
{
"path": "util/token.go",
"chars": 614,
"preview": "package util\n\nimport (\n\t\"crypto/hmac\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"time\"\n)\n\n// GenerateToke"
},
{
"path": "util/traffic_route_signer.go",
"chars": 4179,
"preview": "package util\n\nimport (\n\t\"bytes\"\n\t\"crypto/hmac\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\""
},
{
"path": "util/update/apply.go",
"chars": 2326,
"preview": "// Based on https://github.com/inconshreveable/go-update/blob/7a872911e5b39953310f0a04161f0d50c7e63755/apply.go\n\npackage"
},
{
"path": "util/update/apply_test.go",
"chars": 1294,
"preview": "// Based on https://github.com/inconshreveable/go-update/blob/7a872911e5b39953310f0a04161f0d50c7e63755/apply_test.go\n\npa"
},
{
"path": "util/update/arch.go",
"chars": 653,
"preview": "// Based on https://github.com/creativeprojects/go-selfupdate/blob/v1.1.1/arch.go\n\npackage update\n\nimport (\n\t\"fmt\"\n\t\"run"
},
{
"path": "util/update/arm.go",
"chars": 204,
"preview": "// Based on https://github.com/creativeprojects/go-selfupdate/blob/v1.1.1/arm.go\n\npackage update\n\nimport (\n\t// unsafe 用于"
},
{
"path": "util/update/decompress.go",
"chars": 2162,
"preview": "// Based on https://github.com/creativeprojects/go-selfupdate/blob/v1.1.1/decompress.go\n\npackage update\n\nimport (\n\t\"arch"
},
{
"path": "util/update/decompress_test.go",
"chars": 1723,
"preview": "// Based on https://github.com/creativeprojects/go-selfupdate/blob/v1.1.1/decompress_test.go\n\npackage update\n\nimport (\n\t"
},
{
"path": "util/update/detect.go",
"chars": 2467,
"preview": "// Based on https://github.com/creativeprojects/go-selfupdate/blob/v1.1.1/detect.go\n\npackage update\n\nimport (\n\t\"fmt\"\n\t\"l"
},
{
"path": "util/update/errors.go",
"chars": 264,
"preview": "// Based on https://github.com/creativeprojects/go-selfupdate/blob/v1.1.1/errors.go\n\npackage update\n\nimport \"errors\"\n\nva"
},
{
"path": "util/update/latest.go",
"chars": 506,
"preview": "// Based on https://github.com/creativeprojects/go-selfupdate/blob/v1.1.1/release.go\n\npackage update\n\nimport \"github.com"
},
{
"path": "util/update/package.go",
"chars": 1817,
"preview": "package update\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"runtime\"\n\n\t\"github.com/jeessy2/ddns-go/v6/util\"\n\t\"github.com/jeessy"
},
{
"path": "util/update/release.go",
"chars": 1508,
"preview": "// Based on https://github.com/creativeprojects/go-selfupdate/blob/v1.1.1/github_release.go\n// and https://github.com/cr"
},
{
"path": "util/update/update.go",
"chars": 366,
"preview": "// Based on https://github.com/creativeprojects/go-selfupdate/blob/v1.1.1/update.go\n\npackage update\n\nimport (\n\t\"io\"\n\t\"pa"
},
{
"path": "util/user.go",
"chars": 602,
"preview": "package util\r\n\r\nimport (\r\n\t\"os\"\r\n)\r\n\r\nconst ConfigFilePathENV = \"DDNS_CONFIG_FILE_PATH\"\r\n\r\n// GetConfigFilePath 获得配置文件路径"
},
{
"path": "util/wait_internet.go",
"chars": 980,
"preview": "package util\n\nimport (\n\t\"strings\"\n\t\"time\"\n)\n\n// Wait blocks until the Internet is connected.\n//\n// See also:\n//\n// - h"
},
{
"path": "web/auth.go",
"chars": 1592,
"preview": "package web\n\nimport (\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/jeessy2/ddns-go/v6/config\"\n\t\"github.com/jeessy2/ddns-go/v6/util\""
},
{
"path": "web/login.go",
"chars": 3476,
"preview": "package web\n\nimport (\n\t\"embed\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"html/template\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/jee"
},
{
"path": "web/login.html",
"chars": 3493,
"preview": "<!DOCTYPE html>\n<html>\n\n<head>\n <meta charset=\"utf-8\" />\n <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n <m"
},
{
"path": "web/logout.go",
"chars": 435,
"preview": "package web\n\nimport (\n\t\"net/http\"\n\t\"time\"\n)\n\nfunc Logout(w http.ResponseWriter, r *http.Request) {\n\t// 覆盖cookieInSystem\n"
},
{
"path": "web/logs.go",
"chars": 846,
"preview": "package web\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n)\n\n// MemoryLogs 内存中的日志\ntype MemoryLogs struct {\n\t"
},
{
"path": "web/return_json.go",
"chars": 583,
"preview": "package web\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n)\n\n// Result Result\ntype Result struct {\n\tCode int // 状态\n\tMsg"
},
{
"path": "web/save.go",
"chars": 3622,
"preview": "package web\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/jeessy2/ddns-go/v6/config\"\n\t\"github.com/jees"
},
{
"path": "web/webhookTest.go",
"chars": 1122,
"preview": "package web\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\n\t\"github.com/jeessy2/ddns-go/v6/config\"\n\t\"github.com/jeessy2/ddns-go"
},
{
"path": "web/writing.go",
"chars": 3875,
"preview": "package web\n\nimport (\n\t\"embed\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"html/template\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/jeess"
},
{
"path": "web/writing.html",
"chars": 35906,
"preview": "<!DOCTYPE html>\n<html>\n\n<head>\n <meta charset=\"utf-8\" />\n <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n <m"
}
]
About this extraction
This page contains the full source code of the jeessy2/ddns-go GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 115 files (369.7 KB), approximately 119.0k tokens, and a symbol index with 574 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.