Compare commits
107 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e5a69e5fb | ||
|
|
055f80bf9b | ||
|
|
917ff10563 | ||
|
|
c331e33178 | ||
|
|
5a7bc230b8 | ||
|
|
b7a0a8a7bc | ||
|
|
139a0dd03b | ||
|
|
27e8251d11 | ||
|
|
6ba9360156 | ||
|
|
0c3d538de8 | ||
|
|
a33c09accf | ||
|
|
68bb6ab654 | ||
|
|
835fbb9ef6 | ||
|
|
9c6ca258e1 | ||
|
|
d573f771a8 | ||
|
|
035a219f9f | ||
|
|
19069f50ec | ||
|
|
5de21916a3 | ||
|
|
3d5a512d1e | ||
|
|
9f7a158367 | ||
|
|
1681de11d2 | ||
|
|
88d3be685e | ||
|
|
aa3e7e1b75 | ||
|
|
af542b44a9 | ||
|
|
5b1e51534f | ||
|
|
637c82f407 | ||
|
|
e1f1d6af34 | ||
|
|
c13035a344 | ||
|
|
af5d2561d2 | ||
|
|
395cb7a62c | ||
|
|
5d74ff1142 | ||
|
|
37db83e5b7 | ||
|
|
41a1cff0ae | ||
|
|
4f5fad0d32 | ||
|
|
5651772837 | ||
|
|
09dc25d336 | ||
|
|
a88ee29755 | ||
|
|
de7fe3cb1d | ||
|
|
7a2f9f06b1 | ||
|
|
d695f72963 | ||
|
|
aff13a02fb | ||
|
|
7fbb5261c8 | ||
|
|
2da94f1462 | ||
|
|
ea4d125663 | ||
|
|
f64de0353d | ||
|
|
20411b650f | ||
|
|
3bb130d055 | ||
|
|
1d85bf75b0 | ||
|
|
c2c5c5cd70 | ||
|
|
a09073da12 | ||
|
|
4bb5e265ab | ||
|
|
b452d504c8 | ||
|
|
f76790426d | ||
|
|
f650e47fe5 | ||
|
|
dc0dd43017 | ||
|
|
c827ee4801 | ||
|
|
db2a6bc288 | ||
|
|
d66ccffaf6 | ||
|
|
eeff02e63b | ||
|
|
90ae6ee268 | ||
|
|
0fc5a8e848 | ||
|
|
ec013c0f25 | ||
|
|
856cc05881 | ||
|
|
75d4a30c1f | ||
|
|
8aa869b2f8 | ||
|
|
52e977ce1d | ||
|
|
0bbbf5ed39 | ||
|
|
fbd5c65a6a | ||
|
|
4646e7f7e5 | ||
|
|
3006cb712b | ||
|
|
fde566fe67 | ||
|
|
a17792d33c | ||
|
|
d1af1d029f | ||
|
|
a5b417901c | ||
|
|
065083781e | ||
|
|
0af5dc2cd9 | ||
|
|
1c918fcaa4 | ||
|
|
299c520c2b | ||
|
|
3343d943d6 | ||
|
|
7744357b61 | ||
|
|
14f552e970 | ||
|
|
f2d1fc692b | ||
|
|
dd12fa7c9b | ||
|
|
0991b3e3c9 | ||
|
|
d18cd65374 | ||
|
|
8eed75b495 | ||
|
|
46b9c45e1b | ||
|
|
ed82e15349 | ||
|
|
7b2203ccca | ||
|
|
945013579f | ||
|
|
3ee50d0c20 | ||
|
|
f2fe21934c | ||
|
|
07aa5e0043 | ||
|
|
01f010e35e | ||
|
|
cd3a587e43 | ||
|
|
439da9c09f | ||
|
|
5c2e60a828 | ||
|
|
d542ee03b5 | ||
|
|
92f8cc2802 | ||
|
|
978ac5d62b | ||
|
|
4d214d7f52 | ||
|
|
f463d07d0b | ||
|
|
9a908d7d6b | ||
|
|
11c852ee91 | ||
|
|
32608e9f47 | ||
|
|
77b5fda6fb | ||
|
|
efdd560ee4 |
25
.github/workflows/e2e.yml
vendored
Normal file
25
.github/workflows/e2e.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
name: E2E Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: ${{ github.ref_name != 'master' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
e2e-tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Run E2E Tests
|
||||||
|
run: |
|
||||||
|
cd test/e2e
|
||||||
|
docker compose up --build --abort-on-container-exit --exit-code-from tester
|
||||||
40
.github/workflows/go_cov.yml
vendored
Normal file
40
.github/workflows/go_cov.yml
vendored
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
name: Go Coverage
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
schedule:
|
||||||
|
- cron: '15 */12 * * *'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: ${{ github.ref_name != 'master' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Build and Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.23'
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: go build -v ./...
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: go test -v -race -covermode=atomic -coverprofile=coverage.out ./...
|
||||||
|
|
||||||
|
- name: Send coverage
|
||||||
|
uses: coverallsapp/github-action@v2
|
||||||
|
with:
|
||||||
|
file: coverage.out
|
||||||
|
format: golang
|
||||||
29
.github/workflows/golangci-lint.yml
vendored
Normal file
29
.github/workflows/golangci-lint.yml
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
name: golangci-lint
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: ${{ github.ref_name != 'master' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
golangci:
|
||||||
|
name: lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
steps:
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: 1.23
|
||||||
|
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Run golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v7
|
||||||
|
with:
|
||||||
|
version: v2.0.2
|
||||||
43
.github/workflows/release.yml
vendored
Normal file
43
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
name: goreleaser
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
goreleaser:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
- name: Import GPG key
|
||||||
|
id: import_gpg
|
||||||
|
uses: crazy-max/ghaction-import-gpg@v6
|
||||||
|
with:
|
||||||
|
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
|
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
- name: Run GoReleaser
|
||||||
|
uses: goreleaser/goreleaser-action@v6
|
||||||
|
|
||||||
|
with:
|
||||||
|
distribution: goreleaser
|
||||||
|
version: latest
|
||||||
|
args: release --clean
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -3,3 +3,5 @@ acme-dns.db
|
|||||||
acme-dns.log
|
acme-dns.log
|
||||||
.vagrant
|
.vagrant
|
||||||
coverage.out
|
coverage.out
|
||||||
|
.idea/
|
||||||
|
dist/
|
||||||
|
|||||||
30
.golangci.yaml
Normal file
30
.golangci.yaml
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
version: "2"
|
||||||
|
linters:
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
presets:
|
||||||
|
- comments
|
||||||
|
- common-false-positives
|
||||||
|
- legacy
|
||||||
|
- std-error-handling
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
|
issues:
|
||||||
|
max-issues-per-linter: 0
|
||||||
|
max-same-issues: 0
|
||||||
|
formatters:
|
||||||
|
enable:
|
||||||
|
- gofmt
|
||||||
|
- goimports
|
||||||
|
settings:
|
||||||
|
goimports:
|
||||||
|
local-prefixes:
|
||||||
|
- github.com/acme-dns/acme-dns
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
@ -1,17 +1,35 @@
|
|||||||
builds:
|
builds:
|
||||||
- binary: acme-dns
|
- binary: acme-dns
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=1
|
- CGO_ENABLED=0
|
||||||
goos:
|
goos:
|
||||||
- linux
|
- linux
|
||||||
goarch:
|
goarch:
|
||||||
- 386
|
|
||||||
- amd64
|
- amd64
|
||||||
|
|
||||||
archive:
|
archives:
|
||||||
|
- id: tgz
|
||||||
format: tar.gz
|
format: tar.gz
|
||||||
files:
|
files:
|
||||||
- LICENSE
|
- LICENSE
|
||||||
- README.md
|
- README.md
|
||||||
- Dockerfile
|
- Dockerfile
|
||||||
- config.cfg
|
- config.cfg
|
||||||
|
- acme-dns.service
|
||||||
|
|
||||||
|
signs:
|
||||||
|
- artifacts: checksum
|
||||||
|
args: ["--batch", "-u", "{{ .Env.GPG_FINGERPRINT }}", "--output", "${signature}", "--detach-sign", "${artifact}"]
|
||||||
|
|
||||||
|
dockers:
|
||||||
|
- image_templates:
|
||||||
|
- "joohoi/acme-dns:{{ .Tag }}"
|
||||||
|
- "joohoi/acme-dns:latest"
|
||||||
|
dockerfile: Dockerfile.release
|
||||||
|
build_flag_templates:
|
||||||
|
- "--pull"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.name={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
|
||||||
|
|||||||
13
.travis.yml
13
.travis.yml
@ -1,13 +0,0 @@
|
|||||||
language: go
|
|
||||||
go:
|
|
||||||
- 1.9
|
|
||||||
env:
|
|
||||||
- "PATH=/home/travis/gopath/bin:$PATH"
|
|
||||||
before_install:
|
|
||||||
- go get github.com/golang/lint/golint
|
|
||||||
- go get github.com/mattn/goveralls
|
|
||||||
script:
|
|
||||||
- go vet
|
|
||||||
- golint -set_exit_status
|
|
||||||
- go test -race -v
|
|
||||||
- $HOME/gopath/bin/goveralls -ignore main.go -v -service=travis-ci
|
|
||||||
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"gopls": {
|
||||||
|
"formatting.local": "github.com/acme-dns/acme-dns"
|
||||||
|
}
|
||||||
|
}
|
||||||
59
CHANGELOG.md
Normal file
59
CHANGELOG.md
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## v2.0
|
||||||
|
- Update goreleaser configuration and add a GitHub action to build a release on new version tags (#395)
|
||||||
|
- Huge refactoring and modernization (#325)
|
||||||
|
|
||||||
|
## v1.1
|
||||||
|
- Add timeout to golangci job (#369)
|
||||||
|
- Update deps to support go 1.23 (#368)
|
||||||
|
- Bump dependencies (#334)
|
||||||
|
|
||||||
|
## v1.0
|
||||||
|
- New
|
||||||
|
- Refactoring of the codebase to something more robust
|
||||||
|
- Changed
|
||||||
|
- Updated dependencies
|
||||||
|
- v0.8
|
||||||
|
- NOTE: configuration option: "api_domain" deprecated!
|
||||||
|
- New
|
||||||
|
- Automatic HTTP API certificate provisioning using DNS challenges making acme-dns able to acquire certificates even with HTTP api not being accessible from public internet.
|
||||||
|
- Configuration value for "tls": "letsencryptstaging". Setting it will help you to debug possible issues with HTTP API certificate acquiring process. This is the new default value.
|
||||||
|
- Changed
|
||||||
|
- Fixed: EDNS0 support
|
||||||
|
- Migrated from autocert to [certmagic](https://github.com/mholt/certmagic) for HTTP API certificate handling
|
||||||
|
- v0.7.2
|
||||||
|
- Changed
|
||||||
|
- Fixed: Regression error of not being able to answer to incoming random-case requests.
|
||||||
|
- Fixed: SOA record added to a correct header field in NXDOMAIN responses.
|
||||||
|
- v0.7.1
|
||||||
|
- Changed
|
||||||
|
- Fixed: SOA record correctly added to the TCP DNS server when using both, UDP and TCP servers.
|
||||||
|
- v0.7
|
||||||
|
- New
|
||||||
|
- Added an endpoint to perform health checks
|
||||||
|
- Changed
|
||||||
|
- A new protocol selection for DNS server "both", that binds both - UDP and TCP ports.
|
||||||
|
- Refactored DNS server internals.
|
||||||
|
- Handle some aspects of DNS spec better.
|
||||||
|
- v0.6
|
||||||
|
- New
|
||||||
|
- Command line flag `-c` to specify location of config file.
|
||||||
|
- Proper refusal of dynamic update requests.
|
||||||
|
- Release signing
|
||||||
|
- Changed
|
||||||
|
- Better error messages for goroutines
|
||||||
|
- v0.5
|
||||||
|
- New
|
||||||
|
- Configurable certificate cache directory
|
||||||
|
- Changed
|
||||||
|
- Process wide umask to ensure created files are only readable by the user running acme-dns
|
||||||
|
- Replaced package that handles UUIDs because of a flaw in the original package
|
||||||
|
- Updated dependencies
|
||||||
|
- Better error messages
|
||||||
|
- v0.4 Clear error messages for bad TXT record content, proper handling of static CNAME records, fixed IP address parsing from the request, added option to disable registration endpoint in the configuration.
|
||||||
|
- v0.3.2 Dockerfile was fixed for users using autocert feature
|
||||||
|
- v0.3.1 Added goreleaser for distributing binary builds of the releases
|
||||||
|
- v0.3 Changed autocert to use HTTP-01 challenges, as TLS-SNI is disabled by Let's Encrypt
|
||||||
|
- v0.2 Now powered by httprouter, support wildcard certificates, Docker images
|
||||||
|
- v0.1 Initial release
|
||||||
14
Dockerfile
14
Dockerfile
@ -1,16 +1,17 @@
|
|||||||
FROM golang:1.9.2-alpine AS builder
|
FROM golang:alpine AS builder
|
||||||
LABEL maintainer="joona@kuori.org"
|
LABEL maintainer="joona@kuori.org"
|
||||||
|
|
||||||
RUN apk add --update gcc musl-dev git
|
RUN apk add --update git
|
||||||
|
|
||||||
RUN go get github.com/joohoi/acme-dns
|
ENV GOPATH /tmp/buildcache
|
||||||
WORKDIR /go/src/github.com/joohoi/acme-dns
|
RUN git clone https://github.com/joohoi/acme-dns /tmp/acme-dns
|
||||||
RUN CGO_ENABLED=1 go build
|
WORKDIR /tmp/acme-dns
|
||||||
|
RUN CGO_ENABLED=0 go build
|
||||||
|
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
|
|
||||||
WORKDIR /root/
|
WORKDIR /root/
|
||||||
COPY --from=builder /go/src/github.com/joohoi/acme-dns .
|
COPY --from=builder /tmp/acme-dns .
|
||||||
RUN mkdir -p /etc/acme-dns
|
RUN mkdir -p /etc/acme-dns
|
||||||
RUN mkdir -p /var/lib/acme-dns
|
RUN mkdir -p /var/lib/acme-dns
|
||||||
RUN rm -rf ./config.cfg
|
RUN rm -rf ./config.cfg
|
||||||
@ -19,3 +20,4 @@ RUN apk --no-cache add ca-certificates && update-ca-certificates
|
|||||||
VOLUME ["/etc/acme-dns", "/var/lib/acme-dns"]
|
VOLUME ["/etc/acme-dns", "/var/lib/acme-dns"]
|
||||||
ENTRYPOINT ["./acme-dns"]
|
ENTRYPOINT ["./acme-dns"]
|
||||||
EXPOSE 53 80 443
|
EXPOSE 53 80 443
|
||||||
|
EXPOSE 53/udp
|
||||||
|
|||||||
12
Dockerfile.release
Normal file
12
Dockerfile.release
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
RUN apk --no-cache add ca-certificates && update-ca-certificates
|
||||||
|
RUN mkdir -p /etc/acme-dns
|
||||||
|
RUN mkdir -p /var/lib/acme-dns
|
||||||
|
|
||||||
|
COPY acme-dns /usr/local/bin/acme-dns
|
||||||
|
|
||||||
|
VOLUME ["/etc/acme-dns", "/var/lib/acme-dns"]
|
||||||
|
ENTRYPOINT ["acme-dns"]
|
||||||
|
EXPOSE 53 80 443
|
||||||
|
EXPOSE 53/udp
|
||||||
225
Gopkg.lock
generated
225
Gopkg.lock
generated
@ -1,225 +0,0 @@
|
|||||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
|
||||||
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/BurntSushi/toml"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "b26d9c308763d68093482582cea63d69be07a0f0"
|
|
||||||
version = "v0.3.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/ajg/form"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "cc2954064ec9ea8d93917f0f87456e11d7b881ad"
|
|
||||||
version = "v1.5"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/davecgh/go-spew"
|
|
||||||
packages = ["spew"]
|
|
||||||
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
|
||||||
version = "v1.1.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/erikstmartin/go-testdb"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "8d10e4a1bae52cd8b81ffdec3445890d6dccab3d"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/fatih/structs"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "a720dfa8df582c51dee1b36feabb906bde1588bd"
|
|
||||||
version = "v1.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/gavv/httpexpect"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "c44a6d7bb636b17e880a53998a7f7061a56ffacb"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/gavv/monotime"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "6f8212e8d10df7383609d3c377ca08884d8f3ec0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/google/go-querystring"
|
|
||||||
packages = ["query"]
|
|
||||||
revision = "53e6ce116135b80d037921a7fdd5138cf32d7a8a"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/imkira/go-interpol"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "5accad8134979a6ac504d456a6c7f1c53da237ca"
|
|
||||||
version = "v1.1.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/julienschmidt/httprouter"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "8c199fb6259ffc1af525cc3ad52ee60ba8359669"
|
|
||||||
version = "v1.1"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/klauspost/compress"
|
|
||||||
packages = ["flate","gzip","zlib"]
|
|
||||||
revision = "6c8db69c4b49dd4df1fff66996cf556176d0b9bf"
|
|
||||||
version = "v1.2.1"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/klauspost/cpuid"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "ae7887de9fa5d2db4eaa8174a7eff2c1ac00f2da"
|
|
||||||
version = "v1.1"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/klauspost/crc32"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "cb6bfca970f6908083f26f39a79009d608efd5cd"
|
|
||||||
version = "v1.1"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/lib/pq"
|
|
||||||
packages = [".","oid"]
|
|
||||||
revision = "27ea5d92de30060e7121ddd543fe14e9a327e0cc"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/mattn/go-sqlite3"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "6c771bb9887719704b210e87e934f08be014bdb1"
|
|
||||||
version = "v1.6.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/miekg/dns"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "5ec25f2a5044291b6c8abf43ed8a201da241e69e"
|
|
||||||
version = "v1.0.3"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/moul/http2curl"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "9ac6cf4d929b2fa8fd2d2e6dec5bb0feb4f4911d"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/pmezard/go-difflib"
|
|
||||||
packages = ["difflib"]
|
|
||||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
|
||||||
version = "v1.0.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/rs/cors"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "7af7a1e09ba336d2ea14b1ce73bf693c6837dbf6"
|
|
||||||
version = "v1.2"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/satori/go.uuid"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "f58768cc1a7a7e77a3bd49e98cdd21419399b6a3"
|
|
||||||
version = "v1.2.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/sergi/go-diff"
|
|
||||||
packages = ["diffmatchpatch"]
|
|
||||||
revision = "1744e2970ca51c86172c8190fadad617561ed6e7"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/sirupsen/logrus"
|
|
||||||
packages = [".","hooks/test"]
|
|
||||||
revision = "d682213848ed68c0a260ca37d6dd5ace8423f5ba"
|
|
||||||
version = "v1.0.4"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/stretchr/testify"
|
|
||||||
packages = ["assert","require"]
|
|
||||||
revision = "b91bfb9ebec76498946beb6af7c0230c7cc7ba6c"
|
|
||||||
version = "v1.2.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/valyala/bytebufferpool"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "e746df99fe4a3986f4d4f79e13c1e0117ce9c2f7"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/valyala/fasthttp"
|
|
||||||
packages = [".","fasthttputil","stackless"]
|
|
||||||
revision = "e5f51c11919d4f66400334047b897ef0a94c6f3c"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/xeipuuv/gojsonpointer"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "6fe8760cad3569743d51ddbb243b26f8456742dc"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/xeipuuv/gojsonreference"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "e02fc20de94c78484cd5ffb007f8af96be030a45"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/xeipuuv/gojsonschema"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "511d08a359d14c0dd9c4302af52ee9abb6f93c2a"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/yalp/jsonpath"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "31a79c7593bb93eb10b163650d4a3e6ca190e4dc"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/yudai/gojsondiff"
|
|
||||||
packages = [".","formatter"]
|
|
||||||
revision = "7b1b7adf999dab73a6eb02669c3d82dbb27a3dd6"
|
|
||||||
version = "1.0.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/yudai/golcs"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "ecda9a501e8220fae3b4b600c3db4b0ba22cfc68"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "golang.org/x/crypto"
|
|
||||||
packages = ["acme","acme/autocert","bcrypt","blowfish","ed25519","ed25519/internal/edwards25519","ssh/terminal"]
|
|
||||||
revision = "a6600008915114d9c087fad9f03d75087b1a74df"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "golang.org/x/net"
|
|
||||||
packages = ["bpf","idna","internal/iana","internal/socket","ipv4","ipv6","publicsuffix"]
|
|
||||||
revision = "5ccada7d0a7ba9aeb5d3aca8d3501b4c2a509fec"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "golang.org/x/sys"
|
|
||||||
packages = ["unix","windows"]
|
|
||||||
revision = "af50095a40f9041b3b38960738837185c26e9419"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "golang.org/x/text"
|
|
||||||
packages = ["collate","collate/build","internal/colltab","internal/gen","internal/tag","internal/triegen","internal/ucd","language","secure/bidirule","transform","unicode/bidi","unicode/cldr","unicode/norm","unicode/rangetable"]
|
|
||||||
revision = "e19ae1496984b1c655b8044a65c0300a3c878dd3"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "gopkg.in/DATA-DOG/go-sqlmock.v1"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "d76b18b42f285b792bf985118980ce9eacea9d10"
|
|
||||||
version = "v1.3.0"
|
|
||||||
|
|
||||||
[solve-meta]
|
|
||||||
analyzer-name = "dep"
|
|
||||||
analyzer-version = 1
|
|
||||||
inputs-digest = "991ecc43a6e9bcfe3c39169d7509ee821076b0b75bbf7cb38ec06db3041cd009"
|
|
||||||
solver-name = "gps-cdcl"
|
|
||||||
solver-version = 1
|
|
||||||
76
Gopkg.toml
76
Gopkg.toml
@ -1,76 +0,0 @@
|
|||||||
|
|
||||||
# Gopkg.toml example
|
|
||||||
#
|
|
||||||
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
|
|
||||||
# for detailed Gopkg.toml documentation.
|
|
||||||
#
|
|
||||||
# required = ["github.com/user/thing/cmd/thing"]
|
|
||||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
|
||||||
#
|
|
||||||
# [[constraint]]
|
|
||||||
# name = "github.com/user/project"
|
|
||||||
# version = "1.0.0"
|
|
||||||
#
|
|
||||||
# [[constraint]]
|
|
||||||
# name = "github.com/user/project2"
|
|
||||||
# branch = "dev"
|
|
||||||
# source = "github.com/myfork/project2"
|
|
||||||
#
|
|
||||||
# [[override]]
|
|
||||||
# name = "github.com/x/y"
|
|
||||||
# version = "2.4.0"
|
|
||||||
|
|
||||||
# Need to pin fasthttp so it doesn't get the old version from branch names
|
|
||||||
required = ["github.com/valyala/fasthttp"]
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/valyala/fasthttp"
|
|
||||||
branch = "master"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/BurntSushi/toml"
|
|
||||||
version = "0.3.0"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/erikstmartin/go-testdb"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/gavv/httpexpect"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/julienschmidt/httprouter"
|
|
||||||
version = "1.1.0"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/lib/pq"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/mattn/go-sqlite3"
|
|
||||||
version = "1.6.0"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/miekg/dns"
|
|
||||||
version = "1.0.3"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/rs/cors"
|
|
||||||
version = "1.2.0"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/satori/go.uuid"
|
|
||||||
version = "1.2.0"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/sirupsen/logrus"
|
|
||||||
version = "1.0.4"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "golang.org/x/crypto"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "gopkg.in/DATA-DOG/go-sqlmock.v1"
|
|
||||||
version = "1.3.0"
|
|
||||||
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2016 Joona Hoikkala
|
Copyright (c) 2016-2026 Joona Hoikkala
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
229
README.md
229
README.md
@ -8,9 +8,11 @@ A simplified DNS server with a RESTful HTTP API to provide a simple way to autom
|
|||||||
Many DNS servers do not provide an API to enable automation for the ACME DNS challenges. Those which do, give the keys way too much power.
|
Many DNS servers do not provide an API to enable automation for the ACME DNS challenges. Those which do, give the keys way too much power.
|
||||||
Leaving the keys laying around your random boxes is too often a requirement to have a meaningful process automation.
|
Leaving the keys laying around your random boxes is too often a requirement to have a meaningful process automation.
|
||||||
|
|
||||||
Acme-dns provides a simple API exclusively for TXT record updates and should be used with ACME magic "\_acme-challenge" - subdomain CNAME records. This way, in the unfortunate exposure of API keys, the effetcs are limited to the subdomain TXT record in question.
|
Acme-dns provides a simple API exclusively for TXT record updates and should be used with ACME magic "\_acme-challenge" - subdomain CNAME records. This way, in the unfortunate exposure of API keys, the effects are limited to the subdomain TXT record in question.
|
||||||
|
|
||||||
So basically it boils down to **accessibility** and **security**
|
So basically it boils down to **accessibility** and **security**.
|
||||||
|
|
||||||
|
For longer explanation of the underlying issue and other proposed solutions, see a blog post on the topic from EFF deeplinks blog: https://www.eff.org/deeplinks/2018/02/technical-deep-dive-securing-automation-acme-dns-challenge-validation
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- Simplified DNS server, serving your ACME DNS challenges (TXT)
|
- Simplified DNS server, serving your ACME DNS challenges (TXT)
|
||||||
@ -22,13 +24,16 @@ So basically it boils down to **accessibility** and **security**
|
|||||||
- Simple deployment (it's Go after all)
|
- Simple deployment (it's Go after all)
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
A client application for acme-dns with support for Certbot authentication hooks is available at: [https://github.com/acme-dns/acme-dns-client](https://github.com/acme-dns/acme-dns-client).
|
||||||
|
|
||||||
[](https://asciinema.org/a/94903)
|
[](https://asciinema.org/a/94903)
|
||||||
|
|
||||||
Using acme-dns is a three-step process (provided you already have the self-hosted server set up):
|
Using acme-dns is a three-step process (provided you already have the self-hosted server set up):
|
||||||
|
|
||||||
- Get credentials and unique subdomain (simple POST request to eg. https://auth.acme-dns.io/register)
|
- Get credentials and unique subdomain (simple POST request to eg. https://auth.acme-dns.io/register)
|
||||||
- Create a (ACME magic) CNAME record to your existing zone, pointing to the subdomain you got from the registration. (eg. `_acme-challenge.domainiwantcertfor.tld. CNAME a097455b-52cc-4569-90c8-7a4b97c6eba8.auth.example.org` )
|
- Create a (ACME magic) CNAME record to your existing zone, pointing to the subdomain you got from the registration. (eg. `_acme-challenge.domainiwantcertfor.tld. CNAME a097455b-52cc-4569-90c8-7a4b97c6eba8.auth.example.org` )
|
||||||
- Use your credentials to POST a new DNS challenge values to an acme-dns server for the CA to validate them off of.
|
- Use your credentials to POST new DNS challenge values to an acme-dns server for the CA to validate from.
|
||||||
- Crontab and forget.
|
- Crontab and forget.
|
||||||
|
|
||||||
## API
|
## API
|
||||||
@ -37,9 +42,9 @@ Using acme-dns is a three-step process (provided you already have the self-hoste
|
|||||||
|
|
||||||
The method returns a new unique subdomain and credentials needed to update your record.
|
The method returns a new unique subdomain and credentials needed to update your record.
|
||||||
Fulldomain is where you can point your own `_acme-challenge` subdomain CNAME record to.
|
Fulldomain is where you can point your own `_acme-challenge` subdomain CNAME record to.
|
||||||
With the credentials, you can update the TXT response in the service to match the challenge token, later referred as \_\_\_validation\_token\_recieved\_from\_the\_ca\_\_\_, given out by the Certificate Authority.
|
With the credentials, you can update the TXT response in the service to match the challenge token, later referred as \_\_\_validation\_token\_received\_from\_the\_ca\_\_\_, given out by the Certificate Authority.
|
||||||
|
|
||||||
**Optional:**: You can POST JSON data to limit the /update requests to predefined source networks using CIDR notation.
|
**Optional:**: You can POST JSON data to limit the `/update` requests to predefined source networks using CIDR notation.
|
||||||
|
|
||||||
```POST /register```
|
```POST /register```
|
||||||
|
|
||||||
@ -79,14 +84,14 @@ The method allows you to update the TXT answer contents of your unique subdomain
|
|||||||
#### Required headers
|
#### Required headers
|
||||||
| Header name | Description | Example |
|
| Header name | Description | Example |
|
||||||
| ------------- |--------------------------------------------|-------------------------------------------------------|
|
| ------------- |--------------------------------------------|-------------------------------------------------------|
|
||||||
| X-Api-User | UUIDv4 username recieved from registration | `X-Api-User: c36f50e8-4632-44f0-83fe-e070fef28a10` |
|
| X-Api-User | UUIDv4 username received from registration | `X-Api-User: c36f50e8-4632-44f0-83fe-e070fef28a10` |
|
||||||
| X-Api-Key | Password recieved from registration | `X-Api-Key: htB9mR9DYgcu9bX_afHF62erXaH2TS7bg9KW3F7Z` |
|
| X-Api-Key | Password received from registration | `X-Api-Key: htB9mR9DYgcu9bX_afHF62erXaH2TS7bg9KW3F7Z` |
|
||||||
|
|
||||||
#### Example input
|
#### Example input
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"subdomain": "8e5700ea-a4bf-41c7-8a77-e990661dcc6a",
|
"subdomain": "8e5700ea-a4bf-41c7-8a77-e990661dcc6a",
|
||||||
"txt": "___validation_token_recieved_from_the_ca___"
|
"txt": "___validation_token_received_from_the_ca___"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -95,83 +100,154 @@ The method allows you to update the TXT answer contents of your unique subdomain
|
|||||||
```Status: 200 OK```
|
```Status: 200 OK```
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"txt": "___validation_token_recieved_from_the_ca___"
|
"txt": "___validation_token_received_from_the_ca___"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Health check endpoint
|
||||||
|
|
||||||
|
The method can be used to check readiness and/or liveness of the server. It will return status code 200 on success or won't be reachable.
|
||||||
|
|
||||||
|
```GET /health```
|
||||||
|
|
||||||
## Self-hosted
|
## Self-hosted
|
||||||
|
|
||||||
You are encouraged to run your own acme-dns instance, because you are effectively authorizing the acme-dns server to act on your behalf in providing the answer to challengeing CA, making the instance able to request (and get issued) a TLS certificate for the domain that has CNAME pointing to it.
|
You are encouraged to run your own acme-dns instance, because you are effectively authorizing the acme-dns server to act on your behalf in providing the answer to the challenging CA, making the instance able to request (and get issued) a TLS certificate for the domain that has CNAME pointing to it.
|
||||||
|
|
||||||
Check out how in the INSTALL section.
|
See the INSTALL section for information on how to do this.
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1) Install [Go 1.9 or newer](https://golang.org/doc/install)
|
1) Install [Go 1.13 or newer](https://golang.org/doc/install).
|
||||||
|
|
||||||
2) Clone this repo: `git clone https://github.com/joohoi/acme-dns $GOPATH/src/acme-dns`
|
2) Build acme-dns:
|
||||||
|
```
|
||||||
|
git clone https://github.com/joohoi/acme-dns
|
||||||
|
cd acme-dns
|
||||||
|
export GOPATH=/tmp/acme-dns
|
||||||
|
go build
|
||||||
|
```
|
||||||
|
|
||||||
3) Build ACME-DNS: `go build`
|
3) Move the built acme-dns binary to a directory in your $PATH, for example:
|
||||||
|
`sudo mv acme-dns /usr/local/bin`
|
||||||
|
|
||||||
4) Edit config.cfg to suit your needs (see [configuration](#configuration))
|
4) Edit config.cfg to suit your needs (see [configuration](#configuration)). `acme-dns` will read the configuration file from `/etc/acme-dns/config.cfg` or `./config.cfg`, or a location specified with the `-c` flag.
|
||||||
|
|
||||||
5) Run acme-dns. Please note that acme-dns needs to open a privileged port (53, domain), so it needs to be run with elevated privileges.
|
5) If your system has systemd, you can optionally install acme-dns as a service so that it will start on boot and be tracked by systemd. This also allows us to add the `CAP_NET_BIND_SERVICE` capability so that acme-dns can be run by a user other than root.
|
||||||
|
|
||||||
## Using Docker
|
1) Make sure that you have moved the configuration file to `/etc/acme-dns/config.cfg` so that acme-dns can access it globally.
|
||||||
|
|
||||||
1) Pull the latest acme-dns Docker image: `docker pull joohoi/acme-dns`
|
2) Move the acme-dns executable from `~/go/bin/acme-dns` to `/usr/local/bin/acme-dns` (Any location will work, just be sure to change `acme-dns.service` to match).
|
||||||
|
|
||||||
|
3) Create a minimal acme-dns user: `sudo adduser --system --gecos "acme-dns Service" --disabled-password --group --home /var/lib/acme-dns acme-dns`.
|
||||||
|
|
||||||
|
4) Move the systemd service unit from `acme-dns.service` to `/etc/systemd/system/acme-dns.service`.
|
||||||
|
|
||||||
|
5) Reload systemd units: `sudo systemctl daemon-reload`.
|
||||||
|
|
||||||
|
6) Enable acme-dns on boot: `sudo systemctl enable acme-dns.service`.
|
||||||
|
|
||||||
|
7) Run acme-dns: `sudo systemctl start acme-dns.service`.
|
||||||
|
|
||||||
|
6) If you did not install the systemd service, run `acme-dns`. Please note that acme-dns needs to open a privileged port (53, domain), so it needs to be run with elevated privileges.
|
||||||
|
|
||||||
|
### Using Docker
|
||||||
|
|
||||||
|
1) Pull the latest acme-dns Docker image: `docker pull joohoi/acme-dns`.
|
||||||
|
|
||||||
2) Create directories: `config` for the configuration file, and `data` for the sqlite3 database.
|
2) Create directories: `config` for the configuration file, and `data` for the sqlite3 database.
|
||||||
|
|
||||||
3) Copy [configuration template](https://raw.githubusercontent.com/joohoi/acme-dns/master/config.cfg) to `config/config.cfg`
|
3) Copy [configuration template](https://raw.githubusercontent.com/joohoi/acme-dns/master/config.cfg) to `config/config.cfg`.
|
||||||
|
|
||||||
4) Modify the config.cfg to suit your needs.
|
4) Modify the `config.cfg` to suit your needs.
|
||||||
|
|
||||||
5) Run Docker, this example expects that you have `port = "80"` in your config.cfg:
|
5) Run Docker, this example expects that you have `port = "80"` in your `config.cfg`:
|
||||||
```
|
```
|
||||||
docker run --rm --name acmedns \
|
docker run --rm --name acmedns \
|
||||||
-p 53:53 \
|
-p 53:53 \
|
||||||
|
-p 53:53/udp \
|
||||||
-p 80:80 \
|
-p 80:80 \
|
||||||
-v /path/to/your/config:/etc/acme-dns:ro \
|
-v /path/to/your/config:/etc/acme-dns:ro \
|
||||||
-v /path/to/your/data:/var/lib/acme-dns \
|
-v /path/to/your/data:/var/lib/acme-dns \
|
||||||
-d joohoi/acme-dns
|
-d joohoi/acme-dns
|
||||||
```
|
```
|
||||||
|
|
||||||
## Docker Compose
|
### Docker Compose
|
||||||
|
|
||||||
1) Create directories: `config` for the configuration file, and `data` for the sqlite3 database.
|
1) Create directories: `config` for the configuration file, and `data` for the sqlite3 database.
|
||||||
|
|
||||||
2) Copy [configuration template](https://raw.githubusercontent.com/joohoi/acme-dns/master/config.cfg) to `config/config.cfg`
|
2) Copy [configuration template](https://raw.githubusercontent.com/joohoi/acme-dns/master/config.cfg) to `config/config.cfg`.
|
||||||
|
|
||||||
3) Copy [docker-compose.yml from the project](https://raw.githubusercontent.com/joohoi/acme-dns/master/docker-compose.yml), or create your own.
|
3) Copy [docker-compose.yml from the project](https://raw.githubusercontent.com/joohoi/acme-dns/master/docker-compose.yml), or create your own.
|
||||||
|
|
||||||
4) Edit the `config/config.cfg` and `docker-compose.yml` to suit your needs, and run `docker-compose up -d`
|
4) Edit the `config/config.cfg` and `docker-compose.yml` to suit your needs, and run `docker-compose up -d`.
|
||||||
|
|
||||||
|
## DNS Records
|
||||||
|
|
||||||
|
Note: In this documentation:
|
||||||
|
- `auth.example.org` is the hostname of the acme-dns server
|
||||||
|
- acme-dns will serve `*.auth.example.org` records
|
||||||
|
- `198.51.100.1` is the **public** IP address of the system running acme-dns
|
||||||
|
|
||||||
|
These values should be changed based on your environment.
|
||||||
|
|
||||||
|
You will need to add some DNS records on your domain's regular DNS server:
|
||||||
|
- `NS` record for `auth.example.org` pointing to `auth.example.org` (this means, that `auth.example.org` is responsible for any `*.auth.example.org` records)
|
||||||
|
- `A` record for `auth.example.org` pointing to `198.51.100.1`
|
||||||
|
- If using IPv6, an `AAAA` record pointing to the IPv6 address.
|
||||||
|
- Each domain you will be authenticating will need a `_acme-challenge` `CNAME` subdomain added. The [client](README.md#clients) you use will explain how to do this.
|
||||||
|
|
||||||
|
## Testing It Out
|
||||||
|
|
||||||
|
You may want to test that acme-dns is working before using it for real queries.
|
||||||
|
|
||||||
|
1) Confirm that DNS lookups for the acme-dns subdomain works as expected: `dig auth.example.org`.
|
||||||
|
|
||||||
|
2) Call the `/register` API endpoint to register a test domain:
|
||||||
|
```
|
||||||
|
$ curl -X POST https://auth.example.org/register
|
||||||
|
{"username":"eabcdb41-d89f-4580-826f-3e62e9755ef2","password":"pbAXVjlIOE01xbut7YnAbkhMQIkcwoHO0ek2j4Q0","fulldomain":"d420c923-bbd7-4056-ab64-c3ca54c9b3cf.auth.example.org","subdomain":"d420c923-bbd7-4056-ab64-c3ca54c9b3cf","allowfrom":[]}
|
||||||
|
```
|
||||||
|
|
||||||
|
3) Call the `/update` API endpoint to set a test TXT record. Pass the `username`, `password` and `subdomain` received from the `register` call performed above:
|
||||||
|
```
|
||||||
|
$ curl -X POST \
|
||||||
|
-H "X-Api-User: eabcdb41-d89f-4580-826f-3e62e9755ef2" \
|
||||||
|
-H "X-Api-Key: pbAXVjlIOE01xbut7YnAbkhMQIkcwoHO0ek2j4Q0" \
|
||||||
|
-d '{"subdomain": "d420c923-bbd7-4056-ab64-c3ca54c9b3cf", "txt": "___validation_token_received_from_the_ca___"}' \
|
||||||
|
https://auth.example.org/update
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: The `txt` field must be exactly 43 characters long, otherwise acme-dns will reject it
|
||||||
|
|
||||||
|
4) Perform a DNS lookup to the test subdomain to confirm the updated TXT record is being served:
|
||||||
|
```
|
||||||
|
$ dig -t txt @auth.example.org d420c923-bbd7-4056-ab64-c3ca54c9b3cf.auth.example.org
|
||||||
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
[general]
|
[general]
|
||||||
# dns interface
|
# DNS interface. Note that systemd-resolved may reserve port 53 on 127.0.0.53
|
||||||
listen = ":53"
|
# In this case acme-dns will error out and you will need to define the listening interface
|
||||||
# protocol, "udp", "udp4", "udp6" or "tcp", "tcp4", "tcp6"
|
# for example: listen = "127.0.0.1:53"
|
||||||
protocol = "udp"
|
listen = "127.0.0.1:53"
|
||||||
|
# protocol, "both", "both4", "both6", "udp", "udp4", "udp6" or "tcp", "tcp4", "tcp6"
|
||||||
|
protocol = "both"
|
||||||
# domain name to serve the requests off of
|
# domain name to serve the requests off of
|
||||||
domain = "auth.example.org"
|
domain = "auth.example.org"
|
||||||
# zone name server
|
# zone name server
|
||||||
nsname = "ns1.auth.example.org"
|
nsname = "auth.example.org"
|
||||||
# admin email address, where @ is substituted with .
|
# admin email address, where @ is substituted with .
|
||||||
nsadmin = "admin.example.org"
|
nsadmin = "admin.example.org"
|
||||||
# predefined records served in addition to the TXT
|
# predefined records served in addition to the TXT
|
||||||
records = [
|
records = [
|
||||||
# default A
|
# domain pointing to the public IP of your acme-dns server
|
||||||
"auth.example.org. A 192.168.1.100",
|
"auth.example.org. A 198.51.100.1",
|
||||||
# A
|
# specify that auth.example.org will resolve any *.auth.example.org records
|
||||||
"ns1.auth.example.org. A 192.168.1.100",
|
"auth.example.org. NS auth.example.org.",
|
||||||
"ns2.auth.example.org. A 192.168.1.100",
|
|
||||||
# NS
|
|
||||||
"auth.example.org. NS ns1.auth.example.org.",
|
|
||||||
"auth.example.org. NS ns2.auth.example.org.",
|
|
||||||
]
|
]
|
||||||
# debug messages from CORS etc
|
# debug messages from CORS etc
|
||||||
debug = false
|
debug = false
|
||||||
@ -180,25 +256,34 @@ debug = false
|
|||||||
# Database engine to use, sqlite3 or postgres
|
# Database engine to use, sqlite3 or postgres
|
||||||
engine = "sqlite3"
|
engine = "sqlite3"
|
||||||
# Connection string, filename for sqlite3 and postgres://$username:$password@$host/$db_name for postgres
|
# Connection string, filename for sqlite3 and postgres://$username:$password@$host/$db_name for postgres
|
||||||
connection = "acme-dns.db"
|
# Please note that the default Docker image uses path /var/lib/acme-dns/acme-dns.db for sqlite3
|
||||||
|
connection = "/var/lib/acme-dns/acme-dns.db"
|
||||||
# connection = "postgres://user:password@localhost/acmedns_db"
|
# connection = "postgres://user:password@localhost/acmedns_db"
|
||||||
|
|
||||||
[api]
|
[api]
|
||||||
# domain name to listen requests for, mandatory if using tls = "letsencrypt"
|
# listen ip eg. 127.0.0.1
|
||||||
api_domain = ""
|
ip = "0.0.0.0"
|
||||||
# autocert HTTP port, eg. 80 for answering Let's Encrypt HTTP-01 challenges. Mandatory if using tls = "letsencrypt".
|
# disable registration endpoint
|
||||||
autocert_port = "80"
|
disable_registration = false
|
||||||
# listen port, eg. 443 for default HTTPS
|
# listen port, eg. 443 for default HTTPS
|
||||||
port = "8080"
|
port = "443"
|
||||||
# possible values: "letsencrypt", "cert", "none"
|
# possible values: "letsencrypt", "letsencryptstaging", "cert", "none"
|
||||||
tls = "none"
|
tls = "letsencryptstaging"
|
||||||
# only used if tls = "cert"
|
# only used if tls = "cert"
|
||||||
tls_cert_privkey = "/etc/tls/example.org/privkey.pem"
|
tls_cert_privkey = "/etc/tls/example.org/privkey.pem"
|
||||||
tls_cert_fullchain = "/etc/tls/example.org/fullchain.pem"
|
tls_cert_fullchain = "/etc/tls/example.org/fullchain.pem"
|
||||||
|
# only used if tls = "letsencrypt"
|
||||||
|
acme_cache_dir = "api-certs"
|
||||||
|
# optional e-mail address to which Let's Encrypt will send expiration notices for the API's cert
|
||||||
|
notification_email = ""
|
||||||
# CORS AllowOrigins, wildcards can be used
|
# CORS AllowOrigins, wildcards can be used
|
||||||
corsorigins = [
|
corsorigins = [
|
||||||
"*"
|
"*"
|
||||||
]
|
]
|
||||||
|
# use HTTP header to get the client ip
|
||||||
|
use_header = false
|
||||||
|
# header name to pull the ip address / list of ip addresses from
|
||||||
|
header_name = "X-Forwarded-For"
|
||||||
|
|
||||||
[logconfig]
|
[logconfig]
|
||||||
# logging level: "error", "warning", "info" or "debug"
|
# logging level: "error", "warning", "info" or "debug"
|
||||||
@ -209,18 +294,50 @@ logtype = "stdout"
|
|||||||
# logfile = "./acme-dns.log"
|
# logfile = "./acme-dns.log"
|
||||||
# format, either "json" or "text"
|
# format, either "json" or "text"
|
||||||
logformat = "text"
|
logformat = "text"
|
||||||
# use HTTP header to get the client ip
|
|
||||||
use_header = false
|
|
||||||
# header name to pull the ip address / list of ip addresses from
|
|
||||||
header_name = "X-Forwarded-For"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Changelog
|
## HTTPS API
|
||||||
- v0.3.2 Dockerfile was fixed for users using autocert feature
|
|
||||||
- v0.3.1 Added goreleaser for distributing binary builds of the releases
|
The RESTful acme-dns API can be exposed over HTTPS in two ways:
|
||||||
- v0.3 Changed autocert to use HTTP-01 challenges, as TLS-SNI is disabled by Let's Encrypt
|
|
||||||
- v0.2 Now powered by httprouter, support wildcard certificates, Docker images
|
1. Using `tls = "letsencrypt"` and letting acme-dns issue its own certificate
|
||||||
- v0.1 Initial release
|
automatically with Let's Encrypt.
|
||||||
|
1. Using `tls = "cert"` and providing your own HTTPS certificate chain and
|
||||||
|
private key with `tls_cert_fullchain` and `tls_cert_privkey`.
|
||||||
|
|
||||||
|
Where possible the first option is recommended. This is the easiest and safest
|
||||||
|
way to have acme-dns expose its API over HTTPS.
|
||||||
|
|
||||||
|
**Warning**: If you choose to use `tls = "cert"` you must take care that the
|
||||||
|
certificate *does not expire*! If it does and the ACME client you use to issue the
|
||||||
|
certificate depends on the ACME DNS API to update TXT records you will be stuck
|
||||||
|
in a position where the API certificate has expired but it can't be renewed
|
||||||
|
because the ACME client will refuse to connect to the ACME DNS API it needs to
|
||||||
|
use for the renewal.
|
||||||
|
|
||||||
|
## Clients
|
||||||
|
|
||||||
|
- acme.sh: [https://github.com/Neilpang/acme.sh](https://github.com/Neilpang/acme.sh)
|
||||||
|
- Certify The Web: [https://github.com/webprofusion/certify](https://github.com/webprofusion/certify)
|
||||||
|
- cert-manager: [https://github.com/jetstack/cert-manager](https://github.com/jetstack/cert-manager)
|
||||||
|
- Lego: [https://github.com/xenolf/lego](https://github.com/xenolf/lego)
|
||||||
|
- Posh-ACME: [https://github.com/rmbolger/Posh-ACME](https://github.com/rmbolger/Posh-ACME)
|
||||||
|
- Sewer: [https://github.com/komuw/sewer](https://github.com/komuw/sewer)
|
||||||
|
- Traefik: [https://github.com/containous/traefik](https://github.com/containous/traefik)
|
||||||
|
- Windows ACME Simple (WACS): [https://www.win-acme.com](https://www.win-acme.com)
|
||||||
|
|
||||||
|
### Authentication hooks
|
||||||
|
|
||||||
|
- acme-dns-client with Certbot authentication hook: [https://github.com/acme-dns/acme-dns-client](https://github.com/acme-dns/acme-dns-client)
|
||||||
|
- Certbot authentication hook in Python: [https://github.com/joohoi/acme-dns-certbot-joohoi](https://github.com/joohoi/acme-dns-certbot-joohoi)
|
||||||
|
- Certbot authentication hook in Go: [https://github.com/koesie10/acme-dns-certbot-hook](https://github.com/koesie10/acme-dns-certbot-hook)
|
||||||
|
|
||||||
|
### Libraries
|
||||||
|
|
||||||
|
- Generic client library in Python ([PyPI](https://pypi.python.org/pypi/pyacmedns/)): [https://github.com/joohoi/pyacmedns](https://github.com/joohoi/pyacmedns)
|
||||||
|
- Generic client library in Go: [https://github.com/cpu/goacmedns](https://github.com/cpu/goacmedns)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
@ -235,4 +352,4 @@ If you have an idea for improvement, please open an new issue or feel free to wr
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
acme-dns is released under the [MIT License](http://www.opensource.org/licenses/MIT).
|
acme-dns is released under the [MIT License](https://www.opensource.org/licenses/MIT).
|
||||||
2
Vagrantfile
vendored
2
Vagrantfile
vendored
@ -1,7 +1,7 @@
|
|||||||
# -*- mode: ruby -*-
|
# -*- mode: ruby -*-
|
||||||
# vi: set ft=ruby :
|
# vi: set ft=ruby :
|
||||||
|
|
||||||
# Vagratnfile for running integration tests with PostgreSQL
|
# Vagrantfile for running integration tests with PostgreSQL
|
||||||
|
|
||||||
VAGRANTFILE_API_VERSION = "2"
|
VAGRANTFILE_API_VERSION = "2"
|
||||||
|
|
||||||
|
|||||||
14
acme-dns.service
Normal file
14
acme-dns.service
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Limited DNS server with RESTful HTTP API to handle ACME DNS challenges easily and securely
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=acme-dns
|
||||||
|
Group=acme-dns
|
||||||
|
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||||
|
WorkingDirectory=~
|
||||||
|
ExecStart=/usr/local/bin/acme-dns
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
81
acmetxt.go
81
acmetxt.go
@ -1,81 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/satori/go.uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ACMETxt is the default structure for the user controlled record
|
|
||||||
type ACMETxt struct {
|
|
||||||
Username uuid.UUID
|
|
||||||
Password string
|
|
||||||
ACMETxtPost
|
|
||||||
AllowFrom cidrslice
|
|
||||||
}
|
|
||||||
|
|
||||||
// ACMETxtPost holds the DNS part of the ACMETxt struct
|
|
||||||
type ACMETxtPost struct {
|
|
||||||
Subdomain string `json:"subdomain"`
|
|
||||||
Value string `json:"txt"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// cidrslice is a list of allowed cidr ranges
|
|
||||||
type cidrslice []string
|
|
||||||
|
|
||||||
func (c *cidrslice) JSON() string {
|
|
||||||
ret, _ := json.Marshal(c.ValidEntries())
|
|
||||||
return string(ret)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cidrslice) ValidEntries() []string {
|
|
||||||
valid := []string{}
|
|
||||||
for _, v := range *c {
|
|
||||||
_, _, err := net.ParseCIDR(v)
|
|
||||||
if err == nil {
|
|
||||||
valid = append(valid, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return valid
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if IP belongs to an allowed net
|
|
||||||
func (a ACMETxt) allowedFrom(ip string) bool {
|
|
||||||
remoteIP := net.ParseIP(ip)
|
|
||||||
// Range not limited
|
|
||||||
if len(a.AllowFrom.ValidEntries()) == 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
for _, v := range a.AllowFrom.ValidEntries() {
|
|
||||||
_, vnet, _ := net.ParseCIDR(v)
|
|
||||||
if vnet.Contains(remoteIP) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Go through list (most likely from headers) to check for the IP.
|
|
||||||
// Reason for this is that some setups use reverse proxy in front of acme-dns
|
|
||||||
func (a ACMETxt) allowedFromList(ips []string) bool {
|
|
||||||
if len(ips) == 0 {
|
|
||||||
// If no IP provided, check if no whitelist present (everyone has access)
|
|
||||||
return a.allowedFrom("")
|
|
||||||
}
|
|
||||||
for _, v := range ips {
|
|
||||||
if a.allowedFrom(v) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func newACMETxt() ACMETxt {
|
|
||||||
var a = ACMETxt{}
|
|
||||||
password := generatePassword(40)
|
|
||||||
a.Username = uuid.NewV4()
|
|
||||||
a.Password = password
|
|
||||||
a.Subdomain = uuid.NewV4().String()
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
88
api.go
88
api.go
@ -1,88 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/julienschmidt/httprouter"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RegResponse is a struct for registration response JSON
|
|
||||||
type RegResponse struct {
|
|
||||||
Username string `json:"username"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
Fulldomain string `json:"fulldomain"`
|
|
||||||
Subdomain string `json:"subdomain"`
|
|
||||||
Allowfrom []string `json:"allowfrom"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func webRegisterPost(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
|
||||||
var regStatus int
|
|
||||||
var reg []byte
|
|
||||||
aTXT := ACMETxt{}
|
|
||||||
bdata, _ := ioutil.ReadAll(r.Body)
|
|
||||||
if bdata != nil && len(bdata) > 0 {
|
|
||||||
err := json.Unmarshal(bdata, &aTXT)
|
|
||||||
if err != nil {
|
|
||||||
regStatus = http.StatusBadRequest
|
|
||||||
reg = jsonError("malformed_json_payload")
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(regStatus)
|
|
||||||
w.Write(reg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Create new user
|
|
||||||
nu, err := DB.Register(aTXT.AllowFrom)
|
|
||||||
if err != nil {
|
|
||||||
errstr := fmt.Sprintf("%v", err)
|
|
||||||
reg = jsonError(errstr)
|
|
||||||
regStatus = http.StatusInternalServerError
|
|
||||||
log.WithFields(log.Fields{"error": err.Error()}).Debug("Error in registration")
|
|
||||||
} else {
|
|
||||||
log.WithFields(log.Fields{"user": nu.Username.String()}).Debug("Created new user")
|
|
||||||
regStruct := RegResponse{nu.Username.String(), nu.Password, nu.Subdomain + "." + Config.General.Domain, nu.Subdomain, nu.AllowFrom.ValidEntries()}
|
|
||||||
regStatus = http.StatusCreated
|
|
||||||
reg, err = json.Marshal(regStruct)
|
|
||||||
if err != nil {
|
|
||||||
regStatus = http.StatusInternalServerError
|
|
||||||
reg = jsonError("json_error")
|
|
||||||
log.WithFields(log.Fields{"error": "json"}).Debug("Could not marshal JSON")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(regStatus)
|
|
||||||
w.Write(reg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func webUpdatePost(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
|
||||||
var updStatus int
|
|
||||||
var upd []byte
|
|
||||||
// Get user
|
|
||||||
a, ok := r.Context().Value(ACMETxtKey).(ACMETxt)
|
|
||||||
if !ok {
|
|
||||||
log.WithFields(log.Fields{"error": "context"}).Error("Context error")
|
|
||||||
}
|
|
||||||
if validSubdomain(a.Subdomain) && validTXT(a.Value) {
|
|
||||||
err := DB.Update(a)
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{"error": err.Error()}).Debug("Error while trying to update record")
|
|
||||||
updStatus = http.StatusInternalServerError
|
|
||||||
upd = jsonError("db_error")
|
|
||||||
} else {
|
|
||||||
log.WithFields(log.Fields{"subdomain": a.Subdomain, "txt": a.Value}).Debug("TXT updated")
|
|
||||||
updStatus = http.StatusOK
|
|
||||||
upd = []byte("{\"txt\": \"" + a.Value + "\"}")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.WithFields(log.Fields{"error": "subdomain", "subdomain": a.Subdomain, "txt": a.Value}).Debug("Bad update data")
|
|
||||||
updStatus = http.StatusBadRequest
|
|
||||||
upd = jsonError("bad_subdomain")
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(updStatus)
|
|
||||||
w.Write(upd)
|
|
||||||
}
|
|
||||||
87
auth.go
87
auth.go
@ -1,87 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/julienschmidt/httprouter"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
type key int
|
|
||||||
|
|
||||||
// ACMETxtKey is a context key for ACMETxt struct
|
|
||||||
const ACMETxtKey key = 0
|
|
||||||
|
|
||||||
// Auth middleware for update request
|
|
||||||
func Auth(update httprouter.Handle) httprouter.Handle {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
|
||||||
postData := ACMETxt{}
|
|
||||||
userOK := false
|
|
||||||
user, err := getUserFromRequest(r)
|
|
||||||
if err == nil {
|
|
||||||
if updateAllowedFromIP(r, user) {
|
|
||||||
dec := json.NewDecoder(r.Body)
|
|
||||||
err = dec.Decode(&postData)
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{"error": "json_error", "string": err.Error()}).Error("Decode error")
|
|
||||||
}
|
|
||||||
if user.Subdomain == postData.Subdomain {
|
|
||||||
userOK = true
|
|
||||||
} else {
|
|
||||||
log.WithFields(log.Fields{"error": "subdomain_mismatch", "name": postData.Subdomain, "expected": user.Subdomain}).Error("Subdomain mismatch")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.WithFields(log.Fields{"error": "ip_unauthorized"}).Error("Update not allowed from IP")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.WithFields(log.Fields{"error": err.Error()}).Error("Error while trying to get user")
|
|
||||||
}
|
|
||||||
if userOK {
|
|
||||||
// Set user info to the decoded ACMETxt object
|
|
||||||
postData.Username = user.Username
|
|
||||||
postData.Password = user.Password
|
|
||||||
// Set the ACMETxt struct to context to pull in from update function
|
|
||||||
ctx := context.WithValue(r.Context(), ACMETxtKey, postData)
|
|
||||||
update(w, r.WithContext(ctx), p)
|
|
||||||
} else {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
|
||||||
w.Write(jsonError("forbidden"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getUserFromRequest(r *http.Request) (ACMETxt, error) {
|
|
||||||
uname := r.Header.Get("X-Api-User")
|
|
||||||
passwd := r.Header.Get("X-Api-Key")
|
|
||||||
username, err := getValidUsername(uname)
|
|
||||||
if err != nil {
|
|
||||||
return ACMETxt{}, fmt.Errorf("Invalid username: %s: %s", uname, err.Error())
|
|
||||||
}
|
|
||||||
if validKey(passwd) {
|
|
||||||
dbuser, err := DB.GetByUsername(username)
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{"error": err.Error()}).Error("Error while trying to get user")
|
|
||||||
// To protect against timed side channel (never gonna give you up)
|
|
||||||
correctPassword(passwd, "$2a$10$8JEFVNYYhLoBysjAxe2yBuXrkDojBQBkVpXEQgyQyjn43SvJ4vL36")
|
|
||||||
|
|
||||||
return ACMETxt{}, fmt.Errorf("Invalid username: %s", uname)
|
|
||||||
}
|
|
||||||
if correctPassword(passwd, dbuser.Password) {
|
|
||||||
return dbuser, nil
|
|
||||||
}
|
|
||||||
return ACMETxt{}, fmt.Errorf("Invalid password for user %s", uname)
|
|
||||||
}
|
|
||||||
return ACMETxt{}, fmt.Errorf("Invalid key for user %s", uname)
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateAllowedFromIP(r *http.Request, user ACMETxt) bool {
|
|
||||||
if Config.API.UseHeader {
|
|
||||||
ips := getIPListFromHeader(r.Header.Get(Config.API.HeaderName))
|
|
||||||
return user.allowedFromList(ips)
|
|
||||||
}
|
|
||||||
return user.allowedFrom(r.RemoteAddr)
|
|
||||||
}
|
|
||||||
56
config.cfg
56
config.cfg
@ -1,50 +1,50 @@
|
|||||||
[general]
|
[general]
|
||||||
# dns interface
|
# DNS interface. Note that systemd-resolved may reserve port 53 on 127.0.0.53
|
||||||
listen = ":53"
|
# In this case acme-dns will error out and you will need to define the listening interface
|
||||||
# protocol, "udp", "udp4", "udp6" or "tcp", "tcp4", "tcp6"
|
# for example: listen = "127.0.0.1:53"
|
||||||
protocol = "udp"
|
listen = "127.0.0.1:53"
|
||||||
|
# protocol, "both", "both4", "both6", "udp", "udp4", "udp6" or "tcp", "tcp4", "tcp6"
|
||||||
|
protocol = "both"
|
||||||
# domain name to serve the requests off of
|
# domain name to serve the requests off of
|
||||||
domain = "auth.example.org"
|
domain = "auth.example.org"
|
||||||
# zone name server
|
# zone name server
|
||||||
nsname = "ns1.auth.example.org"
|
nsname = "auth.example.org"
|
||||||
# admin email address, where @ is substituted with .
|
# admin email address, where @ is substituted with .
|
||||||
nsadmin = "admin.example.org"
|
nsadmin = "admin.example.org"
|
||||||
# predefined records served in addition to the TXT
|
# predefined records served in addition to the TXT
|
||||||
records = [
|
records = [
|
||||||
# default A
|
# domain pointing to the public IP of your acme-dns server
|
||||||
"auth.example.org. A 192.168.1.100",
|
"auth.example.org. A 198.51.100.1",
|
||||||
# A
|
# specify that auth.example.org will resolve any *.auth.example.org records
|
||||||
"ns1.auth.example.org. A 192.168.1.100",
|
"auth.example.org. NS auth.example.org.",
|
||||||
"ns2.auth.example.org. A 192.168.1.100",
|
|
||||||
# NS
|
|
||||||
"auth.example.org. NS ns1.auth.example.org.",
|
|
||||||
"auth.example.org. NS ns2.auth.example.org.",
|
|
||||||
]
|
]
|
||||||
# debug messages from CORS etc
|
# debug messages from CORS etc
|
||||||
debug = false
|
debug = false
|
||||||
|
|
||||||
[database]
|
[database]
|
||||||
# Database engine to use, sqlite3 or postgres
|
# Database engine to use, sqlite or postgres
|
||||||
engine = "sqlite3"
|
engine = "sqlite"
|
||||||
# Connection string, filename for sqlite3 and postgres://$username:$password@$host/$db_name for postgres
|
# Connection string, filename for sqlite3 and postgres://$username:$password@$host/$db_name for postgres
|
||||||
# Please note that the default Docker image uses path /var/lib/acme-dns/acme-dns.db for sqlite3
|
# Please note that the default Docker image uses path /var/lib/acme-dns/acme-dns.db for sqlite3
|
||||||
connection = "/var/lib/acme-dns/acme-dns.db"
|
connection = "acme-dns.db"
|
||||||
# connection = "postgres://user:password@localhost/acmedns_db"
|
# connection = "postgres://user:password@localhost/acmedns_db"
|
||||||
|
|
||||||
[api]
|
[api]
|
||||||
# domain name to listen requests for, mandatory if using tls = "letsencrypt"
|
|
||||||
api_domain = ""
|
|
||||||
# listen ip eg. 127.0.0.1
|
# listen ip eg. 127.0.0.1
|
||||||
ip = "0.0.0.0"
|
ip = "0.0.0.0"
|
||||||
# autocert HTTP port, eg. 80 for answering Let's Encrypt HTTP-01 challenges. Mandatory if using tls = "letsencrypt".
|
# disable registration endpoint
|
||||||
autocert_port = "80"
|
disable_registration = false
|
||||||
# listen port, eg. 443 for default HTTPS
|
# listen port, eg. 443 for default HTTPS
|
||||||
port = "80"
|
port = "443"
|
||||||
# possible values: "letsencrypt", "cert", "none"
|
# possible values: "letsencrypt", "letsencryptstaging", "cert", "none"
|
||||||
tls = "none"
|
tls = "none"
|
||||||
# only used if tls = "cert"
|
# only used if tls = "cert"
|
||||||
tls_cert_privkey = "/etc/tls/example.org/privkey.pem"
|
tls_cert_privkey = "/etc/tls/example.org/privkey.pem"
|
||||||
tls_cert_fullchain = "/etc/tls/example.org/fullchain.pem"
|
tls_cert_fullchain = "/etc/tls/example.org/fullchain.pem"
|
||||||
|
# only used if tls = "letsencrypt"
|
||||||
|
acme_cache_dir = "api-certs"
|
||||||
|
# optional e-mail address to which Let's Encrypt will send expiration notices for the API's cert
|
||||||
|
notification_email = ""
|
||||||
# CORS AllowOrigins, wildcards can be used
|
# CORS AllowOrigins, wildcards can be used
|
||||||
corsorigins = [
|
corsorigins = [
|
||||||
"*"
|
"*"
|
||||||
@ -56,10 +56,10 @@ header_name = "X-Forwarded-For"
|
|||||||
|
|
||||||
[logconfig]
|
[logconfig]
|
||||||
# logging level: "error", "warning", "info" or "debug"
|
# logging level: "error", "warning", "info" or "debug"
|
||||||
loglevel = "debug"
|
loglevel = "info"
|
||||||
# possible values: stdout, TODO file & integrations
|
# possible values: stdout, file
|
||||||
logtype = "stdout"
|
logtype = "stdout"
|
||||||
# file path for logfile TODO
|
# file path for logfile
|
||||||
# logfile = "./acme-dns.log"
|
logfile = "./acme-dns.log"
|
||||||
# format, either "json" or "text"
|
# format, either "json" or "text"
|
||||||
logformat = "text"
|
logformat = "json"
|
||||||
|
|||||||
106
dns.go
106
dns.go
@ -1,106 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func readQuery(m *dns.Msg) {
|
|
||||||
for _, que := range m.Question {
|
|
||||||
if rr, rc, err := answer(que); err == nil {
|
|
||||||
m.MsgHdr.Rcode = rc
|
|
||||||
for _, r := range rr {
|
|
||||||
m.Answer = append(m.Answer, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func answerTXT(q dns.Question) ([]dns.RR, int, error) {
|
|
||||||
var ra []dns.RR
|
|
||||||
rcode := dns.RcodeNameError
|
|
||||||
subdomain := sanitizeDomainQuestion(q.Name)
|
|
||||||
atxt, err := DB.GetTXTForDomain(subdomain)
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{"error": err.Error()}).Debug("Error while trying to get record")
|
|
||||||
return ra, dns.RcodeNameError, err
|
|
||||||
}
|
|
||||||
for _, v := range atxt {
|
|
||||||
if len(v) > 0 {
|
|
||||||
r := new(dns.TXT)
|
|
||||||
r.Hdr = dns.RR_Header{Name: q.Name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 1}
|
|
||||||
r.Txt = append(r.Txt, v)
|
|
||||||
ra = append(ra, r)
|
|
||||||
rcode = dns.RcodeSuccess
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.WithFields(log.Fields{"domain": q.Name}).Info("Answering TXT question for domain")
|
|
||||||
return ra, rcode, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func answer(q dns.Question) ([]dns.RR, int, error) {
|
|
||||||
if q.Qtype == dns.TypeTXT {
|
|
||||||
return answerTXT(q)
|
|
||||||
}
|
|
||||||
var r []dns.RR
|
|
||||||
var rcode = dns.RcodeSuccess
|
|
||||||
var domain = strings.ToLower(q.Name)
|
|
||||||
var rtype = q.Qtype
|
|
||||||
r, ok := RR.Records[rtype][domain]
|
|
||||||
if !ok {
|
|
||||||
rcode = dns.RcodeNameError
|
|
||||||
}
|
|
||||||
log.WithFields(log.Fields{"qtype": dns.TypeToString[rtype], "domain": domain, "rcode": dns.RcodeToString[rcode]}).Debug("Answering question for domain")
|
|
||||||
return r, rcode, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleRequest(w dns.ResponseWriter, r *dns.Msg) {
|
|
||||||
m := new(dns.Msg)
|
|
||||||
m.SetReply(r)
|
|
||||||
|
|
||||||
if r.Opcode == dns.OpcodeQuery {
|
|
||||||
readQuery(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteMsg(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse config records
|
|
||||||
func (r *Records) Parse(config general) {
|
|
||||||
rrmap := make(map[uint16]map[string][]dns.RR)
|
|
||||||
for _, v := range config.StaticRecords {
|
|
||||||
rr, err := dns.NewRR(strings.ToLower(v))
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{"error": err.Error(), "rr": v}).Warning("Could not parse RR from config")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Add parsed RR to the list
|
|
||||||
rrmap = appendRR(rrmap, rr)
|
|
||||||
}
|
|
||||||
// Create serial
|
|
||||||
serial := time.Now().Format("2006010215")
|
|
||||||
// Add SOA
|
|
||||||
SOAstring := fmt.Sprintf("%s. SOA %s. %s. %s 28800 7200 604800 86400", strings.ToLower(config.Domain), strings.ToLower(config.Nsname), strings.ToLower(config.Nsadmin), serial)
|
|
||||||
soarr, err := dns.NewRR(SOAstring)
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{"error": err.Error(), "soa": SOAstring}).Error("Error while adding SOA record")
|
|
||||||
} else {
|
|
||||||
rrmap = appendRR(rrmap, soarr)
|
|
||||||
}
|
|
||||||
r.Records = rrmap
|
|
||||||
}
|
|
||||||
|
|
||||||
func appendRR(rrmap map[uint16]map[string][]dns.RR, rr dns.RR) map[uint16]map[string][]dns.RR {
|
|
||||||
_, ok := rrmap[rr.Header().Rrtype]
|
|
||||||
if !ok {
|
|
||||||
newrr := make(map[string][]dns.RR)
|
|
||||||
rrmap[rr.Header().Rrtype] = newrr
|
|
||||||
}
|
|
||||||
rrmap[rr.Header().Rrtype][rr.Header().Name] = append(rrmap[rr.Header().Rrtype][rr.Header().Name], rr)
|
|
||||||
log.WithFields(log.Fields{"recordtype": dns.TypeToString[rr.Header().Rrtype], "domain": rr.Header().Name}).Debug("Adding new record type to domain")
|
|
||||||
return rrmap
|
|
||||||
}
|
|
||||||
195
dns_test.go
195
dns_test.go
@ -1,195 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"database/sql/driver"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/erikstmartin/go-testdb"
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
var resolv resolver
|
|
||||||
var server *dns.Server
|
|
||||||
|
|
||||||
type resolver struct {
|
|
||||||
server string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resolver) lookup(host string, qtype uint16) ([]dns.RR, error) {
|
|
||||||
msg := new(dns.Msg)
|
|
||||||
msg.Id = dns.Id()
|
|
||||||
msg.Question = make([]dns.Question, 1)
|
|
||||||
msg.Question[0] = dns.Question{Name: dns.Fqdn(host), Qtype: qtype, Qclass: dns.ClassINET}
|
|
||||||
in, err := dns.Exchange(msg, r.server)
|
|
||||||
if err != nil {
|
|
||||||
return []dns.RR{}, fmt.Errorf("Error querying the server [%v]", err)
|
|
||||||
}
|
|
||||||
if in != nil && in.Rcode != dns.RcodeSuccess {
|
|
||||||
return []dns.RR{}, fmt.Errorf("Received error from the server [%s]", dns.RcodeToString[in.Rcode])
|
|
||||||
}
|
|
||||||
|
|
||||||
return in.Answer, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasExpectedTXTAnswer(answer []dns.RR, cmpTXT string) error {
|
|
||||||
for _, record := range answer {
|
|
||||||
// We expect only one answer, so no need to loop through the answer slice
|
|
||||||
if rec, ok := record.(*dns.TXT); ok {
|
|
||||||
for _, txtValue := range rec.Txt {
|
|
||||||
if txtValue == cmpTXT {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
errmsg := fmt.Sprintf("Got answer of unexpected type [%q]", answer[0])
|
|
||||||
return errors.New(errmsg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return errors.New("Expected answer not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
func findRecordFromMemory(rrstr string, host string, qtype uint16) error {
|
|
||||||
var errmsg = "No record found"
|
|
||||||
arr, _ := dns.NewRR(strings.ToLower(rrstr))
|
|
||||||
if arrQt, ok := RR.Records[qtype]; ok {
|
|
||||||
if arrHst, ok := arrQt[host]; ok {
|
|
||||||
for _, v := range arrHst {
|
|
||||||
if arr.String() == v.String() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
errmsg = "No records for domain"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
errmsg = "No records for this type in DB"
|
|
||||||
}
|
|
||||||
return errors.New(errmsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestQuestionDBError(t *testing.T) {
|
|
||||||
testdb.SetQueryWithArgsFunc(func(query string, args []driver.Value) (result driver.Rows, err error) {
|
|
||||||
columns := []string{"Username", "Password", "Subdomain", "Value", "LastActive"}
|
|
||||||
return testdb.RowsFromSlice(columns, [][]driver.Value{}), errors.New("Prepared query error")
|
|
||||||
})
|
|
||||||
|
|
||||||
defer testdb.Reset()
|
|
||||||
|
|
||||||
tdb, err := sql.Open("testdb", "")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Got error: %v", err)
|
|
||||||
}
|
|
||||||
oldDb := DB.GetBackend()
|
|
||||||
|
|
||||||
DB.SetBackend(tdb)
|
|
||||||
defer DB.SetBackend(oldDb)
|
|
||||||
|
|
||||||
q := dns.Question{Name: dns.Fqdn("whatever.tld"), Qtype: dns.TypeTXT, Qclass: dns.ClassINET}
|
|
||||||
_, rcode, err := answerTXT(q)
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("Expected error but got none")
|
|
||||||
}
|
|
||||||
if rcode != dns.RcodeNameError {
|
|
||||||
t.Errorf("Expected [%s] rcode, but got [%s]", dns.RcodeToString[dns.RcodeNameError], dns.RcodeToString[rcode])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParse(t *testing.T) {
|
|
||||||
var testcfg = general{
|
|
||||||
Domain: ")",
|
|
||||||
Nsname: "ns1.auth.example.org",
|
|
||||||
Nsadmin: "admin.example.org",
|
|
||||||
StaticRecords: []string{},
|
|
||||||
Debug: false,
|
|
||||||
}
|
|
||||||
var testRR Records
|
|
||||||
testRR.Parse(testcfg)
|
|
||||||
if !loggerHasEntryWithMessage("Error while adding SOA record") {
|
|
||||||
t.Errorf("Expected SOA parsing to return error, but did not find one")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResolveA(t *testing.T) {
|
|
||||||
resolv := resolver{server: "0.0.0.0:15353"}
|
|
||||||
answer, err := resolv.lookup("auth.example.org", dns.TypeA)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(answer) > 0 {
|
|
||||||
err = findRecordFromMemory(answer[0].String(), "auth.example.org.", dns.TypeA)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Answer [%s] did not match the expected, got error: [%s], debug: [%q]", answer[0].String(), err, RR.Records)
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
t.Error("No answer for DNS query")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = resolv.lookup("nonexistent.domain.tld", dns.TypeA)
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("Was expecting error because of NXDOMAIN but got none")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResolveTXT(t *testing.T) {
|
|
||||||
resolv := resolver{server: "0.0.0.0:15353"}
|
|
||||||
validTXT := "______________valid_response_______________"
|
|
||||||
|
|
||||||
atxt, err := DB.Register(cidrslice{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Could not initiate db record: [%v]", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
atxt.Value = validTXT
|
|
||||||
err = DB.Update(atxt)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Could not update db record: [%v]", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, test := range []struct {
|
|
||||||
subDomain string
|
|
||||||
expTXT string
|
|
||||||
getAnswer bool
|
|
||||||
validAnswer bool
|
|
||||||
}{
|
|
||||||
{atxt.Subdomain, validTXT, true, true},
|
|
||||||
{atxt.Subdomain, "invalid", true, false},
|
|
||||||
{"a097455b-52cc-4569-90c8-7a4b97c6eba8", validTXT, false, false},
|
|
||||||
} {
|
|
||||||
answer, err := resolv.lookup(test.subDomain+".auth.example.org", dns.TypeTXT)
|
|
||||||
if err != nil {
|
|
||||||
if test.getAnswer {
|
|
||||||
t.Fatalf("Test %d: Expected answer but got: %v", i, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if !test.getAnswer {
|
|
||||||
t.Errorf("Test %d: Expected no answer, but got one.", i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(answer) > 0 {
|
|
||||||
if !test.getAnswer {
|
|
||||||
t.Errorf("Test %d: Expected no answer, but got: [%q]", i, answer)
|
|
||||||
}
|
|
||||||
err = hasExpectedTXTAnswer(answer, test.expTXT)
|
|
||||||
if err != nil {
|
|
||||||
if test.validAnswer {
|
|
||||||
t.Errorf("Test %d: %v", i, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if !test.validAnswer {
|
|
||||||
t.Errorf("Test %d: Answer was not expected to be valid, answer [%q], compared to [%s]", i, answer, test.expTXT)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if test.getAnswer {
|
|
||||||
t.Errorf("Test %d: Expected answer, but didn't get one", i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -8,6 +8,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "443:443"
|
- "443:443"
|
||||||
- "53:53"
|
- "53:53"
|
||||||
|
- "53:53/udp"
|
||||||
- "80:80"
|
- "80:80"
|
||||||
volumes:
|
volumes:
|
||||||
- ./config:/etc/acme-dns:ro
|
- ./config:/etc/acme-dns:ro
|
||||||
|
|||||||
73
go.mod
Normal file
73
go.mod
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
module github.com/joohoi/acme-dns
|
||||||
|
|
||||||
|
go 1.23.0
|
||||||
|
|
||||||
|
toolchain go1.23.5
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/BurntSushi/toml v1.4.0
|
||||||
|
github.com/DATA-DOG/go-sqlmock v1.5.0
|
||||||
|
github.com/caddyserver/certmagic v0.23.0
|
||||||
|
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5
|
||||||
|
github.com/gavv/httpexpect v2.0.0+incompatible
|
||||||
|
github.com/glebarez/go-sqlite v1.20.0
|
||||||
|
github.com/google/uuid v1.3.0
|
||||||
|
github.com/julienschmidt/httprouter v1.3.0
|
||||||
|
github.com/lib/pq v1.10.7
|
||||||
|
github.com/mholt/acmez/v3 v3.1.2
|
||||||
|
github.com/miekg/dns v1.1.65
|
||||||
|
github.com/rs/cors v1.8.3
|
||||||
|
go.uber.org/zap v1.27.0
|
||||||
|
golang.org/x/crypto v0.38.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/caddyserver/zerossl v0.1.3 // indirect
|
||||||
|
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||||
|
go.uber.org/zap/exp v0.3.0 // indirect
|
||||||
|
golang.org/x/sync v0.14.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/ajg/form v1.5.1 // indirect
|
||||||
|
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072 // indirect
|
||||||
|
github.com/fatih/structs v1.1.0 // indirect
|
||||||
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.0 // indirect
|
||||||
|
github.com/imkira/go-interpol v1.1.0 // indirect
|
||||||
|
github.com/klauspost/compress v1.15.13 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||||
|
github.com/libdns/libdns v1.0.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||||
|
github.com/moul/http2curl v1.0.0 // indirect
|
||||||
|
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||||
|
github.com/onsi/gomega v1.24.2 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa // indirect
|
||||||
|
github.com/sergi/go-diff v1.2.0 // indirect
|
||||||
|
github.com/smartystreets/goconvey v1.7.2 // indirect
|
||||||
|
github.com/stretchr/testify v1.8.1 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasthttp v1.43.0 // indirect
|
||||||
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||||
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||||
|
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||||
|
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect
|
||||||
|
github.com/yudai/gojsondiff v1.0.0 // indirect
|
||||||
|
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect
|
||||||
|
github.com/yudai/pp v2.0.1+incompatible // indirect
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
golang.org/x/mod v0.24.0 // indirect
|
||||||
|
golang.org/x/net v0.40.0 // indirect
|
||||||
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
|
golang.org/x/text v0.25.0 // indirect
|
||||||
|
golang.org/x/tools v0.33.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
modernc.org/libc v1.21.5 // indirect
|
||||||
|
modernc.org/mathutil v1.5.0 // indirect
|
||||||
|
modernc.org/memory v1.4.0 // indirect
|
||||||
|
modernc.org/sqlite v1.20.0 // indirect
|
||||||
|
)
|
||||||
271
go.sum
Normal file
271
go.sum
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
|
||||||
|
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
|
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||||
|
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||||
|
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||||
|
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||||
|
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||||
|
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
|
github.com/caddyserver/certmagic v0.23.0 h1:CfpZ/50jMfG4+1J/u2LV6piJq4HOfO6ppOnOf7DkFEU=
|
||||||
|
github.com/caddyserver/certmagic v0.23.0/go.mod h1:9mEZIWqqWoI+Gf+4Trh04MOVPD0tGSxtqsxg87hAIH4=
|
||||||
|
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
|
||||||
|
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
|
||||||
|
github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
|
||||||
|
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
|
||||||
|
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
|
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
|
||||||
|
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
||||||
|
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072 h1:DddqAaWDpywytcG8w/qoQ5sAN8X12d3Z3koB0C3Rxsc=
|
||||||
|
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
|
||||||
|
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||||
|
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
|
github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8=
|
||||||
|
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
|
||||||
|
github.com/glebarez/go-sqlite v1.20.0 h1:6D9uRXq3Kd+W7At+hOU2eIAeahv6qcYfO8jzmvb4Dr8=
|
||||||
|
github.com/glebarez/go-sqlite v1.20.0/go.mod h1:uTnJoqtwMQjlULmljLT73Cg7HB+2X6evsBHODyyq1ak=
|
||||||
|
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||||
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||||
|
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||||
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||||
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
|
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
|
||||||
|
github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=
|
||||||
|
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
|
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
||||||
|
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||||
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||||
|
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
||||||
|
github.com/klauspost/compress v1.15.13 h1:NFn1Wr8cfnenSJSA46lLq4wHCcBzKTSjnBIexDMMOV0=
|
||||||
|
github.com/klauspost/compress v1.15.13/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
|
||||||
|
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/libdns/libdns v1.0.0 h1:IvYaz07JNz6jUQ4h/fv2R4sVnRnm77J/aOuC9B+TQTA=
|
||||||
|
github.com/libdns/libdns v1.0.0/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||||
|
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
|
github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc=
|
||||||
|
github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
|
||||||
|
github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc=
|
||||||
|
github.com/miekg/dns v1.1.65/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck=
|
||||||
|
github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs=
|
||||||
|
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
|
||||||
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
|
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||||
|
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||||
|
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||||
|
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||||
|
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||||
|
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||||
|
github.com/onsi/gomega v1.24.2 h1:J/tulyYK6JwBldPViHJReihxxZ+22FHs0piGjQAvoUE=
|
||||||
|
github.com/onsi/gomega v1.24.2/go.mod h1:gs3J10IS7Z7r7eXRoNJIrNqU4ToQukCJhFtKrWgHWnk=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa h1:tEkEyxYeZ43TR55QU/hsIt9aRGBxbgGuz9CGykjvogY=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/rs/cors v1.8.3 h1:O+qNyWn7Z+F9M0ILBHgMVPuB1xTOucVd5gtaYyXBpRo=
|
||||||
|
github.com/rs/cors v1.8.3/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||||
|
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
|
||||||
|
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||||
|
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
|
||||||
|
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
||||||
|
github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
|
||||||
|
github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasthttp v1.43.0 h1:Gy4sb32C98fbzVWZlTM1oTMdLWGyvxR03VhM6cBIU4g=
|
||||||
|
github.com/valyala/fasthttp v1.43.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY=
|
||||||
|
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||||
|
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||||
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
||||||
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||||
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||||
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||||
|
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||||
|
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||||
|
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=
|
||||||
|
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
|
||||||
|
github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
|
||||||
|
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
|
||||||
|
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
|
||||||
|
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
|
||||||
|
github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI=
|
||||||
|
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
|
||||||
|
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||||
|
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
|
||||||
|
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
|
||||||
|
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
||||||
|
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
|
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
|
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
|
||||||
|
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
|
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||||
|
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||||
|
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||||
|
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||||
|
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||||
|
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||||
|
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||||
|
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||||
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||||
|
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||||
|
modernc.org/cc/v3 v3.37.0/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20=
|
||||||
|
modernc.org/cc/v3 v3.38.1/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20=
|
||||||
|
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
|
||||||
|
modernc.org/ccgo/v3 v3.0.0-20220904174949-82d86e1b6d56/go.mod h1:YSXjPL62P2AMSxBphRHPn7IkzhVHqkvOnRKAKh+W6ZI=
|
||||||
|
modernc.org/ccgo/v3 v3.0.0-20220910160915-348f15de615a/go.mod h1:8p47QxPkdugex9J4n9P2tLZ9bK01yngIVp00g4nomW0=
|
||||||
|
modernc.org/ccgo/v3 v3.16.13-0.20221017192402-261537637ce8/go.mod h1:fUB3Vn0nVPReA+7IG7yZDfjv1TMWjhQP8gCxrFAtL5g=
|
||||||
|
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
|
||||||
|
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
||||||
|
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
||||||
|
modernc.org/libc v1.17.4/go.mod h1:WNg2ZH56rDEwdropAJeZPQkXmDwh+JCA1s/htl6r2fA=
|
||||||
|
modernc.org/libc v1.18.0/go.mod h1:vj6zehR5bfc98ipowQOM2nIDUZnVew/wNC/2tOGS+q0=
|
||||||
|
modernc.org/libc v1.19.0/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0=
|
||||||
|
modernc.org/libc v1.20.3/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0=
|
||||||
|
modernc.org/libc v1.21.4/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI=
|
||||||
|
modernc.org/libc v1.21.5 h1:xBkU9fnHV+hvZuPSRszN0AXDG4M7nwPLwTWwkYcvLCI=
|
||||||
|
modernc.org/libc v1.21.5/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI=
|
||||||
|
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||||
|
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||||
|
modernc.org/memory v1.3.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||||
|
modernc.org/memory v1.4.0 h1:crykUfNSnMAXaOJnnxcSzbUGMqkLWjklJKkBK2nwZwk=
|
||||||
|
modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||||
|
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||||
|
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||||
|
modernc.org/sqlite v1.20.0 h1:80zmD3BGkm8BZ5fUi/4lwJQHiO3GXgIUvZRXpoIfROY=
|
||||||
|
modernc.org/sqlite v1.20.0/go.mod h1:EsYz8rfOvLCiYTy5ZFsOYzoCcRMu98YYkwAcCw5YIYw=
|
||||||
|
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
|
||||||
|
modernc.org/tcl v1.15.0/go.mod h1:xRoGotBZ6dU+Zo2tca+2EqVEeMmOUBzHnhIwq4YrVnE=
|
||||||
|
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
|
modernc.org/z v1.7.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ=
|
||||||
130
main.go
130
main.go
@ -1,111 +1,55 @@
|
|||||||
//+build !test
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"flag"
|
||||||
stdlog "log"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/joohoi/acme-dns/pkg/acmedns"
|
||||||
"github.com/rs/cors"
|
"github.com/joohoi/acme-dns/pkg/api"
|
||||||
log "github.com/sirupsen/logrus"
|
"github.com/joohoi/acme-dns/pkg/database"
|
||||||
"golang.org/x/crypto/acme/autocert"
|
"github.com/joohoi/acme-dns/pkg/nameserver"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
setUmask()
|
||||||
|
configPtr := flag.String("c", "/etc/acme-dns/config.cfg", "config file location")
|
||||||
|
flag.Parse()
|
||||||
// Read global config
|
// Read global config
|
||||||
if fileExists("/etc/acme-dns/config.cfg") {
|
var err error
|
||||||
Config = readConfig("/etc/acme-dns/config.cfg")
|
var logger *zap.Logger
|
||||||
log.WithFields(log.Fields{"file": "/etc/acme-dns/config.cfg"}).Info("Using config file")
|
config, usedConfigFile, err := acmedns.ReadConfig(*configPtr, "./config.cfg")
|
||||||
|
|
||||||
} else {
|
|
||||||
log.WithFields(log.Fields{"file": "./config.cfg"}).Info("Using config file")
|
|
||||||
Config = readConfig("config.cfg")
|
|
||||||
}
|
|
||||||
|
|
||||||
setupLogging(Config.Logconfig.Format, Config.Logconfig.Level)
|
|
||||||
|
|
||||||
// Read the default records in
|
|
||||||
RR.Parse(Config.General)
|
|
||||||
|
|
||||||
// Open database
|
|
||||||
newDB := new(acmedb)
|
|
||||||
err := newDB.Init(Config.Database.Engine, Config.Database.Connection)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Could not open database [%v]", err)
|
fmt.Printf("Error: %s\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
} else {
|
|
||||||
log.Info("Connected to database")
|
|
||||||
}
|
}
|
||||||
DB = newDB
|
logger, err = acmedns.SetupLogging(config)
|
||||||
defer DB.Close()
|
if err != nil {
|
||||||
|
fmt.Printf("Could not set up logging: %s\n", err)
|
||||||
// DNS server
|
os.Exit(1)
|
||||||
startDNS(Config.General.Listen, Config.General.Proto)
|
|
||||||
|
|
||||||
// HTTP API
|
|
||||||
startHTTPAPI()
|
|
||||||
|
|
||||||
log.Debugf("Shutting down...")
|
|
||||||
}
|
|
||||||
|
|
||||||
func startHTTPAPI() {
|
|
||||||
// Setup http logger
|
|
||||||
logger := log.New()
|
|
||||||
logwriter := logger.Writer()
|
|
||||||
defer logwriter.Close()
|
|
||||||
api := httprouter.New()
|
|
||||||
c := cors.New(cors.Options{
|
|
||||||
AllowedOrigins: Config.API.CorsOrigins,
|
|
||||||
AllowedMethods: []string{"GET", "POST"},
|
|
||||||
OptionsPassthrough: false,
|
|
||||||
Debug: Config.General.Debug,
|
|
||||||
})
|
|
||||||
if Config.General.Debug {
|
|
||||||
// Logwriter for saner log output
|
|
||||||
c.Log = stdlog.New(logwriter, "", 0)
|
|
||||||
}
|
}
|
||||||
api.POST("/register", webRegisterPost)
|
// Make sure to flush the zap logger buffer before exiting
|
||||||
api.POST("/update", Auth(webUpdatePost))
|
defer logger.Sync() //nolint:all
|
||||||
|
sugar := logger.Sugar()
|
||||||
|
|
||||||
host := Config.API.IP + ":" + Config.API.Port
|
sugar.Infow("Using config file",
|
||||||
|
"file", usedConfigFile)
|
||||||
cfg := &tls.Config{
|
sugar.Info("Starting up")
|
||||||
MinVersion: tls.VersionTLS12,
|
db, err := database.Init(&config, sugar)
|
||||||
|
// Error channel for servers
|
||||||
|
errChan := make(chan error, 1)
|
||||||
|
api := api.Init(&config, db, sugar, errChan)
|
||||||
|
dnsservers := nameserver.InitAndStart(&config, db, sugar, errChan)
|
||||||
|
go api.Start(dnsservers)
|
||||||
|
if err != nil {
|
||||||
|
sugar.Error(err)
|
||||||
}
|
}
|
||||||
|
for {
|
||||||
switch Config.API.TLS {
|
err = <-errChan
|
||||||
case "letsencrypt":
|
if err != nil {
|
||||||
m := autocert.Manager{
|
sugar.Fatal(err)
|
||||||
Cache: autocert.DirCache("api-certs"),
|
|
||||||
Prompt: autocert.AcceptTOS,
|
|
||||||
HostPolicy: autocert.HostWhitelist(Config.API.Domain),
|
|
||||||
}
|
}
|
||||||
autocerthost := Config.API.IP + ":" + Config.API.AutocertPort
|
|
||||||
log.WithFields(log.Fields{"autocerthost": autocerthost, "domain": Config.API.Domain}).Debug("Opening HTTP port for autocert")
|
|
||||||
go http.ListenAndServe(autocerthost, m.HTTPHandler(nil))
|
|
||||||
cfg.GetCertificate = m.GetCertificate
|
|
||||||
srv := &http.Server{
|
|
||||||
Addr: host,
|
|
||||||
Handler: c.Handler(api),
|
|
||||||
TLSConfig: cfg,
|
|
||||||
ErrorLog: stdlog.New(logwriter, "", 0),
|
|
||||||
}
|
|
||||||
log.WithFields(log.Fields{"host": host, "domain": Config.API.Domain}).Info("Listening HTTPS, using certificate from autocert")
|
|
||||||
log.Fatal(srv.ListenAndServeTLS("", ""))
|
|
||||||
case "cert":
|
|
||||||
srv := &http.Server{
|
|
||||||
Addr: host,
|
|
||||||
Handler: c.Handler(api),
|
|
||||||
TLSConfig: cfg,
|
|
||||||
ErrorLog: stdlog.New(logwriter, "", 0),
|
|
||||||
}
|
|
||||||
log.WithFields(log.Fields{"host": host}).Info("Listening HTTPS")
|
|
||||||
log.Fatal(srv.ListenAndServeTLS(Config.API.TLSCertFullchain, Config.API.TLSCertPrivkey))
|
|
||||||
default:
|
|
||||||
log.WithFields(log.Fields{"host": host}).Info("Listening HTTP")
|
|
||||||
log.Fatal(http.ListenAndServe(host, c.Handler(api)))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
96
main_test.go
96
main_test.go
@ -1,96 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
logrustest "github.com/sirupsen/logrus/hooks/test"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
var loghook = new(logrustest.Hook)
|
|
||||||
|
|
||||||
var (
|
|
||||||
postgres = flag.Bool("postgres", false, "run integration tests against PostgreSQL")
|
|
||||||
)
|
|
||||||
|
|
||||||
var records = []string{
|
|
||||||
"auth.example.org. A 192.168.1.100",
|
|
||||||
"ns1.auth.example.org. A 192.168.1.101",
|
|
||||||
"!''b', unparseable ",
|
|
||||||
"ns2.auth.example.org. A 192.168.1.102",
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
|
||||||
setupTestLogger()
|
|
||||||
setupConfig()
|
|
||||||
RR.Parse(Config.General)
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
newDb := new(acmedb)
|
|
||||||
if *postgres {
|
|
||||||
Config.Database.Engine = "postgres"
|
|
||||||
err := newDb.Init("postgres", "postgres://acmedns:acmedns@localhost/acmedns")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("PostgreSQL integration tests expect database \"acmedns\" running in localhost, with username and password set to \"acmedns\"")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Config.Database.Engine = "sqlite3"
|
|
||||||
_ = newDb.Init("sqlite3", ":memory:")
|
|
||||||
}
|
|
||||||
DB = newDb
|
|
||||||
server := startDNS("0.0.0.0:15353", "udp")
|
|
||||||
exitval := m.Run()
|
|
||||||
server.Shutdown()
|
|
||||||
DB.Close()
|
|
||||||
os.Exit(exitval)
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupConfig() {
|
|
||||||
var dbcfg = dbsettings{
|
|
||||||
Engine: "sqlite3",
|
|
||||||
Connection: ":memory:",
|
|
||||||
}
|
|
||||||
|
|
||||||
var generalcfg = general{
|
|
||||||
Domain: "auth.example.org",
|
|
||||||
Nsname: "ns1.auth.example.org",
|
|
||||||
Nsadmin: "admin.example.org",
|
|
||||||
StaticRecords: records,
|
|
||||||
Debug: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
var httpapicfg = httpapi{
|
|
||||||
Domain: "",
|
|
||||||
Port: "8080",
|
|
||||||
TLS: "none",
|
|
||||||
CorsOrigins: []string{"*"},
|
|
||||||
UseHeader: false,
|
|
||||||
HeaderName: "X-Forwarded-For",
|
|
||||||
}
|
|
||||||
|
|
||||||
var dnscfg = DNSConfig{
|
|
||||||
Database: dbcfg,
|
|
||||||
General: generalcfg,
|
|
||||||
API: httpapicfg,
|
|
||||||
}
|
|
||||||
|
|
||||||
Config = dnscfg
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupTestLogger() {
|
|
||||||
log.SetOutput(ioutil.Discard)
|
|
||||||
log.AddHook(loghook)
|
|
||||||
}
|
|
||||||
|
|
||||||
func loggerHasEntryWithMessage(message string) bool {
|
|
||||||
for _, v := range loghook.Entries {
|
|
||||||
if v.Message == message {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
47
pkg/acmedns/acmetxt.go
Normal file
47
pkg/acmedns/acmetxt.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package acmedns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AllowedFrom Check if IP belongs to an allowed net
|
||||||
|
func (a ACMETxt) AllowedFrom(ip string) bool {
|
||||||
|
remoteIP := net.ParseIP(ip)
|
||||||
|
// Range not limited
|
||||||
|
if len(a.AllowFrom.ValidEntries()) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, v := range a.AllowFrom.ValidEntries() {
|
||||||
|
_, vnet, _ := net.ParseCIDR(v)
|
||||||
|
if vnet.Contains(remoteIP) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllowedFromList Go through list (most likely from headers) to check for the IP.
|
||||||
|
// Reason for this is that some setups use reverse proxy in front of acme-dns
|
||||||
|
func (a ACMETxt) AllowedFromList(ips []string) bool {
|
||||||
|
if len(ips) == 0 {
|
||||||
|
// If no IP provided, check if no whitelist present (everyone has access)
|
||||||
|
return a.AllowedFrom("")
|
||||||
|
}
|
||||||
|
for _, v := range ips {
|
||||||
|
if a.AllowedFrom(v) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewACMETxt() ACMETxt {
|
||||||
|
var a = ACMETxt{}
|
||||||
|
password := generatePassword(40)
|
||||||
|
a.Username = uuid.New()
|
||||||
|
a.Password = password
|
||||||
|
a.Subdomain = uuid.New().String()
|
||||||
|
return a
|
||||||
|
}
|
||||||
38
pkg/acmedns/acmetxt_test.go
Normal file
38
pkg/acmedns/acmetxt_test.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package acmedns
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestAllowedFrom(t *testing.T) {
|
||||||
|
testslice := NewACMETxt()
|
||||||
|
testslice.AllowFrom = []string{"192.168.1.0/24", "2001:db8::/32"}
|
||||||
|
for _, test := range []struct {
|
||||||
|
input string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{"192.168.1.42", true},
|
||||||
|
{"192.168.2.42", false},
|
||||||
|
{"2001:db8:aaaa::", true},
|
||||||
|
{"2001:db9:aaaa::", false},
|
||||||
|
} {
|
||||||
|
if testslice.AllowedFrom(test.input) != test.expected {
|
||||||
|
t.Errorf("Was expecting AllowedFrom to return %t for %s but got %t instead.", test.expected, test.input, !test.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAllowedFromList(t *testing.T) {
|
||||||
|
testslice := ACMETxt{AllowFrom: []string{"192.168.1.0/24", "2001:db8::/32"}}
|
||||||
|
if testslice.AllowedFromList([]string{"192.168.2.2", "1.1.1.1"}) != false {
|
||||||
|
t.Errorf("Was expecting AllowedFromList to return false")
|
||||||
|
}
|
||||||
|
if testslice.AllowedFromList([]string{"192.168.1.2", "1.1.1.1"}) != true {
|
||||||
|
t.Errorf("Was expecting AllowedFromList to return true")
|
||||||
|
}
|
||||||
|
allowfromall := ACMETxt{AllowFrom: []string{}}
|
||||||
|
if allowfromall.AllowedFromList([]string{"192.168.1.2", "1.1.1.1"}) != true {
|
||||||
|
t.Errorf("Expected non-restricted AlloFrom to be allowed")
|
||||||
|
}
|
||||||
|
if allowfromall.AllowedFromList([]string{}) != true {
|
||||||
|
t.Errorf("Expected non-restricted AlloFrom to be allowed for empty list")
|
||||||
|
}
|
||||||
|
}
|
||||||
35
pkg/acmedns/cidrslice.go
Normal file
35
pkg/acmedns/cidrslice.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package acmedns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// cidrslice is a list of allowed cidr ranges
|
||||||
|
type Cidrslice []string
|
||||||
|
|
||||||
|
func (c *Cidrslice) JSON() string {
|
||||||
|
ret, _ := json.Marshal(c.ValidEntries())
|
||||||
|
return string(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cidrslice) IsValid() error {
|
||||||
|
for _, v := range *c {
|
||||||
|
_, _, err := net.ParseCIDR(sanitizeIPv6addr(v))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cidrslice) ValidEntries() []string {
|
||||||
|
valid := []string{}
|
||||||
|
for _, v := range *c {
|
||||||
|
_, _, err := net.ParseCIDR(sanitizeIPv6addr(v))
|
||||||
|
if err == nil {
|
||||||
|
valid = append(valid, sanitizeIPv6addr(v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return valid
|
||||||
|
}
|
||||||
35
pkg/acmedns/cidrslice_test.go
Normal file
35
pkg/acmedns/cidrslice_test.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package acmedns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCidrSlice(t *testing.T) {
|
||||||
|
for i, test := range []struct {
|
||||||
|
input Cidrslice
|
||||||
|
expectedErr bool
|
||||||
|
expectedLen int
|
||||||
|
}{
|
||||||
|
{[]string{"192.168.1.0/24"}, false, 1},
|
||||||
|
{[]string{"shoulderror"}, true, 0},
|
||||||
|
{[]string{"2001:db8:aaaaa::"}, true, 0},
|
||||||
|
{[]string{"192.168.1.0/24", "2001:db8::/32"}, false, 2},
|
||||||
|
} {
|
||||||
|
err := test.input.IsValid()
|
||||||
|
if test.expectedErr && err == nil {
|
||||||
|
t.Errorf("Expected test %d to generate IsValid() error but it didn't", i)
|
||||||
|
}
|
||||||
|
if !test.expectedErr && err != nil {
|
||||||
|
t.Errorf("Expected test %d to pass IsValid() but it generated an error %s", i, err)
|
||||||
|
}
|
||||||
|
outSlice := []string{}
|
||||||
|
err = json.Unmarshal([]byte(test.input.JSON()), &outSlice)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error when unmarshaling Cidrslice JSON: %s", err)
|
||||||
|
}
|
||||||
|
if len(outSlice) != test.expectedLen {
|
||||||
|
t.Errorf("Expected cidrslice JSON to be of length %d, but got %d instead for test %d", test.expectedLen, len(outSlice), i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
82
pkg/acmedns/config.go
Normal file
82
pkg/acmedns/config.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package acmedns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ApiTlsProviderNone = "none"
|
||||||
|
ApiTlsProviderLetsEncrypt = "letsencrypt"
|
||||||
|
ApiTlsProviderLetsEncryptStaging = "letsencryptstaging"
|
||||||
|
ApiTlsProviderCert = "cert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FileIsAccessible(fname string) bool {
|
||||||
|
_, err := os.Stat(fname)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
f, err := os.Open(fname)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func readTomlConfig(fname string) (AcmeDnsConfig, error) {
|
||||||
|
var conf AcmeDnsConfig
|
||||||
|
_, err := toml.DecodeFile(fname, &conf)
|
||||||
|
if err != nil {
|
||||||
|
// Return with config file parsing errors from toml package
|
||||||
|
return conf, err
|
||||||
|
}
|
||||||
|
return prepareConfig(conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareConfig checks that mandatory values exist, and can be used to set default values in the future
|
||||||
|
func prepareConfig(conf AcmeDnsConfig) (AcmeDnsConfig, error) {
|
||||||
|
if conf.Database.Engine == "" {
|
||||||
|
return conf, errors.New("missing database configuration option \"engine\"")
|
||||||
|
}
|
||||||
|
if conf.Database.Connection == "" {
|
||||||
|
return conf, errors.New("missing database configuration option \"connection\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default values for options added to config to keep backwards compatibility with old config
|
||||||
|
if conf.API.ACMECacheDir == "" {
|
||||||
|
conf.API.ACMECacheDir = "api-certs"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch conf.API.TLS {
|
||||||
|
case ApiTlsProviderCert, ApiTlsProviderLetsEncrypt, ApiTlsProviderLetsEncryptStaging, ApiTlsProviderNone:
|
||||||
|
// we have a good value
|
||||||
|
default:
|
||||||
|
return conf, fmt.Errorf("invalid value for api.tls, expected one of [%s, %s, %s, %s]", ApiTlsProviderCert, ApiTlsProviderLetsEncrypt, ApiTlsProviderLetsEncryptStaging, ApiTlsProviderNone)
|
||||||
|
}
|
||||||
|
|
||||||
|
return conf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadConfig(configFile, fallback string) (AcmeDnsConfig, string, error) {
|
||||||
|
var usedConfigFile string
|
||||||
|
var config AcmeDnsConfig
|
||||||
|
var err error
|
||||||
|
if FileIsAccessible(configFile) {
|
||||||
|
usedConfigFile = configFile
|
||||||
|
config, err = readTomlConfig(configFile)
|
||||||
|
} else if FileIsAccessible(fallback) {
|
||||||
|
usedConfigFile = fallback
|
||||||
|
config, err = readTomlConfig(fallback)
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("configuration file not found")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("encountered an error while trying to read configuration file: %w", err)
|
||||||
|
}
|
||||||
|
return config, usedConfigFile, err
|
||||||
|
}
|
||||||
24
pkg/acmedns/interfaces.go
Normal file
24
pkg/acmedns/interfaces.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package acmedns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AcmednsDB interface {
|
||||||
|
Register(cidrslice Cidrslice) (ACMETxt, error)
|
||||||
|
GetByUsername(uuid.UUID) (ACMETxt, error)
|
||||||
|
GetTXTForDomain(string) ([]string, error)
|
||||||
|
Update(ACMETxtPost) error
|
||||||
|
GetBackend() *sql.DB
|
||||||
|
SetBackend(*sql.DB)
|
||||||
|
Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
type AcmednsNS interface {
|
||||||
|
Start(errorChannel chan error)
|
||||||
|
SetOwnAuthKey(key string)
|
||||||
|
SetNotifyStartedFunc(func())
|
||||||
|
ParseRecords()
|
||||||
|
}
|
||||||
46
pkg/acmedns/logging.go
Normal file
46
pkg/acmedns/logging.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package acmedns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetupLogging(config AcmeDnsConfig) (*zap.Logger, error) {
|
||||||
|
var (
|
||||||
|
logger *zap.Logger
|
||||||
|
zapCfg zap.Config
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
logformat := "console"
|
||||||
|
if config.Logconfig.Format == "json" {
|
||||||
|
logformat = "json"
|
||||||
|
}
|
||||||
|
outputPath := "stdout"
|
||||||
|
if config.Logconfig.Logtype == "file" {
|
||||||
|
outputPath = config.Logconfig.File
|
||||||
|
}
|
||||||
|
errorPath := "stderr"
|
||||||
|
if config.Logconfig.Logtype == "file" {
|
||||||
|
errorPath = config.Logconfig.File
|
||||||
|
}
|
||||||
|
|
||||||
|
zapCfg.Level, err = zap.ParseAtomicLevel(config.Logconfig.Level)
|
||||||
|
if err != nil {
|
||||||
|
return logger, err
|
||||||
|
}
|
||||||
|
zapCfg.Encoding = logformat
|
||||||
|
zapCfg.OutputPaths = []string{outputPath}
|
||||||
|
zapCfg.ErrorOutputPaths = []string{errorPath}
|
||||||
|
zapCfg.EncoderConfig = zapcore.EncoderConfig{
|
||||||
|
TimeKey: "time",
|
||||||
|
MessageKey: "msg",
|
||||||
|
LevelKey: "level",
|
||||||
|
EncodeLevel: zapcore.LowercaseLevelEncoder,
|
||||||
|
EncodeTime: zapcore.ISO8601TimeEncoder,
|
||||||
|
}
|
||||||
|
|
||||||
|
logger, err = zapCfg.Build()
|
||||||
|
return logger, err
|
||||||
|
}
|
||||||
36
pkg/acmedns/testdata/test_read_fallback_config.toml
vendored
Normal file
36
pkg/acmedns/testdata/test_read_fallback_config.toml
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
[general]
|
||||||
|
listen = "127.0.0.1:53"
|
||||||
|
protocol = "both"
|
||||||
|
domain = "test.example.org"
|
||||||
|
nsname = "test.example.org"
|
||||||
|
nsadmin = "test.example.org"
|
||||||
|
records = [
|
||||||
|
"test.example.org. A 127.0.0.1",
|
||||||
|
"test.example.org. NS test.example.org.",
|
||||||
|
]
|
||||||
|
debug = true
|
||||||
|
|
||||||
|
[database]
|
||||||
|
engine = "dinosaur"
|
||||||
|
connection = "roar"
|
||||||
|
|
||||||
|
[api]
|
||||||
|
ip = "0.0.0.0"
|
||||||
|
disable_registration = false
|
||||||
|
port = "443"
|
||||||
|
tls = "none"
|
||||||
|
tls_cert_privkey = "/etc/tls/example.org/privkey.pem"
|
||||||
|
tls_cert_fullchain = "/etc/tls/example.org/fullchain.pem"
|
||||||
|
acme_cache_dir = "api-certs"
|
||||||
|
notification_email = ""
|
||||||
|
corsorigins = [
|
||||||
|
"*"
|
||||||
|
]
|
||||||
|
use_header = true
|
||||||
|
header_name = "X-is-gonna-give-it-to-ya"
|
||||||
|
|
||||||
|
[logconfig]
|
||||||
|
loglevel = "info"
|
||||||
|
logtype = "stdout"
|
||||||
|
logfile = "./acme-dns.log"
|
||||||
|
logformat = "json"
|
||||||
72
pkg/acmedns/types.go
Normal file
72
pkg/acmedns/types.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package acmedns
|
||||||
|
|
||||||
|
import "github.com/google/uuid"
|
||||||
|
|
||||||
|
type Account struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
Subdomain string
|
||||||
|
}
|
||||||
|
|
||||||
|
// AcmeDnsConfig holds the config structure
|
||||||
|
type AcmeDnsConfig struct {
|
||||||
|
General general
|
||||||
|
Database dbsettings
|
||||||
|
API httpapi
|
||||||
|
Logconfig logconfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config file general section
|
||||||
|
type general struct {
|
||||||
|
Listen string
|
||||||
|
Proto string `toml:"protocol"`
|
||||||
|
Domain string
|
||||||
|
Nsname string
|
||||||
|
Nsadmin string
|
||||||
|
Debug bool
|
||||||
|
StaticRecords []string `toml:"records"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type dbsettings struct {
|
||||||
|
Engine string
|
||||||
|
Connection string
|
||||||
|
}
|
||||||
|
|
||||||
|
// API config
|
||||||
|
type httpapi struct {
|
||||||
|
Domain string `toml:"api_domain"`
|
||||||
|
IP string
|
||||||
|
DisableRegistration bool `toml:"disable_registration"`
|
||||||
|
AutocertPort string `toml:"autocert_port"`
|
||||||
|
Port string `toml:"port"`
|
||||||
|
TLS string
|
||||||
|
TLSCertPrivkey string `toml:"tls_cert_privkey"`
|
||||||
|
TLSCertFullchain string `toml:"tls_cert_fullchain"`
|
||||||
|
ACMECacheDir string `toml:"acme_cache_dir"`
|
||||||
|
NotificationEmail string `toml:"notification_email"`
|
||||||
|
CorsOrigins []string
|
||||||
|
UseHeader bool `toml:"use_header"`
|
||||||
|
HeaderName string `toml:"header_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logging config
|
||||||
|
type logconfig struct {
|
||||||
|
Level string `toml:"loglevel"`
|
||||||
|
Logtype string `toml:"logtype"`
|
||||||
|
File string `toml:"logfile"`
|
||||||
|
Format string `toml:"logformat"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ACMETxt is the default structure for the user controlled record
|
||||||
|
type ACMETxt struct {
|
||||||
|
Username uuid.UUID
|
||||||
|
Password string
|
||||||
|
ACMETxtPost
|
||||||
|
AllowFrom Cidrslice
|
||||||
|
}
|
||||||
|
|
||||||
|
// ACMETxtPost holds the DNS part of the ACMETxt struct
|
||||||
|
type ACMETxtPost struct {
|
||||||
|
Subdomain string `json:"subdomain"`
|
||||||
|
Value string `json:"txt"`
|
||||||
|
}
|
||||||
40
pkg/acmedns/util.go
Normal file
40
pkg/acmedns/util.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package acmedns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"math/big"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func sanitizeIPv6addr(s string) string {
|
||||||
|
// Remove brackets from IPv6 addresses, net.ParseCIDR needs this
|
||||||
|
re, _ := regexp.Compile(`[\[\]]+`)
|
||||||
|
return re.ReplaceAllString(s, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func SanitizeString(s string) string {
|
||||||
|
// URL safe base64 alphabet without padding as defined in ACME
|
||||||
|
re, _ := regexp.Compile(`[^A-Za-z\-\_0-9]+`)
|
||||||
|
return re.ReplaceAllString(s, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func generatePassword(length int) string {
|
||||||
|
ret := make([]byte, length)
|
||||||
|
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890-_"
|
||||||
|
alphalen := big.NewInt(int64(len(alphabet)))
|
||||||
|
for i := 0; i < length; i++ {
|
||||||
|
c, _ := rand.Int(rand.Reader, alphalen)
|
||||||
|
r := int(c.Int64())
|
||||||
|
ret[i] = alphabet[r]
|
||||||
|
}
|
||||||
|
return string(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CorrectPassword(pw string, hash string) bool {
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(pw)); err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
326
pkg/acmedns/util_test.go
Normal file
326
pkg/acmedns/util_test.go
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
package acmedns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand/v2"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func fakeConfig() AcmeDnsConfig {
|
||||||
|
conf := AcmeDnsConfig{}
|
||||||
|
conf.Logconfig.Logtype = "stdout"
|
||||||
|
return conf
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetupLogging(t *testing.T) {
|
||||||
|
conf := fakeConfig()
|
||||||
|
for i, test := range []struct {
|
||||||
|
format string
|
||||||
|
level string
|
||||||
|
expected zapcore.Level
|
||||||
|
}{
|
||||||
|
{"text", "warn", zap.WarnLevel},
|
||||||
|
{"json", "debug", zap.DebugLevel},
|
||||||
|
{"text", "info", zap.InfoLevel},
|
||||||
|
{"json", "error", zap.ErrorLevel},
|
||||||
|
} {
|
||||||
|
conf.Logconfig.Format = test.format
|
||||||
|
conf.Logconfig.Level = test.level
|
||||||
|
logger, err := SetupLogging(conf)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Got unexpected error: %s", err)
|
||||||
|
} else {
|
||||||
|
if logger.Sugar().Level() != test.expected {
|
||||||
|
t.Errorf("Test %d: Expected loglevel %s but got %s", i, test.expected, logger.Sugar().Level())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetupLoggingError(t *testing.T) {
|
||||||
|
conf := fakeConfig()
|
||||||
|
for _, test := range []struct {
|
||||||
|
format string
|
||||||
|
level string
|
||||||
|
file string
|
||||||
|
errexpected bool
|
||||||
|
}{
|
||||||
|
{"text", "warn", "", false},
|
||||||
|
{"json", "debug", "", false},
|
||||||
|
{"text", "info", "", false},
|
||||||
|
{"json", "error", "", false},
|
||||||
|
{"text", "something", "", true},
|
||||||
|
{"text", "info", "a path with\" in its name.txt", false},
|
||||||
|
} {
|
||||||
|
conf.Logconfig.Format = test.format
|
||||||
|
conf.Logconfig.Level = test.level
|
||||||
|
if test.file != "" {
|
||||||
|
conf.Logconfig.File = test.file
|
||||||
|
conf.Logconfig.Logtype = "file"
|
||||||
|
|
||||||
|
}
|
||||||
|
_, err := SetupLogging(conf)
|
||||||
|
if test.errexpected && err == nil {
|
||||||
|
t.Errorf("Expected error but did not get one for loglevel: %s", err)
|
||||||
|
} else if !test.errexpected && err != nil {
|
||||||
|
t.Errorf("Unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clean up the file zap creates
|
||||||
|
if test.file != "" {
|
||||||
|
_ = os.Remove(test.file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadConfig(t *testing.T) {
|
||||||
|
for i, test := range []struct {
|
||||||
|
inFile []byte
|
||||||
|
output AcmeDnsConfig
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
[]byte("[general]\nlisten = \":53\"\ndebug = true\n[api]\napi_domain = \"something.strange\""),
|
||||||
|
AcmeDnsConfig{
|
||||||
|
General: general{
|
||||||
|
Listen: ":53",
|
||||||
|
Debug: true,
|
||||||
|
},
|
||||||
|
API: httpapi{
|
||||||
|
Domain: "something.strange",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
[]byte("[\x00[[[[[[[[[de\nlisten =]"),
|
||||||
|
AcmeDnsConfig{},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
tmpfile, err := os.CreateTemp("", "acmedns")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Could not create temporary file: %s", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tmpfile.Name())
|
||||||
|
|
||||||
|
if _, err := tmpfile.Write(test.inFile); err != nil {
|
||||||
|
t.Error("Could not write to temporary file")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tmpfile.Close(); err != nil {
|
||||||
|
t.Error("Could not close temporary file")
|
||||||
|
}
|
||||||
|
ret, _, _ := ReadConfig(tmpfile.Name(), "")
|
||||||
|
if ret.General.Listen != test.output.General.Listen {
|
||||||
|
t.Errorf("Test %d: Expected listen value %s, but got %s", i, test.output.General.Listen, ret.General.Listen)
|
||||||
|
}
|
||||||
|
if ret.API.Domain != test.output.API.Domain {
|
||||||
|
t.Errorf("Test %d: Expected HTTP API domain %s, but got %s", i, test.output.API.Domain, ret.API.Domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadConfigFallback(t *testing.T) {
|
||||||
|
var (
|
||||||
|
path string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
testPath := "testdata/test_read_fallback_config.toml"
|
||||||
|
|
||||||
|
path, err = getNonExistentPath()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed getting non existant path: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, used, err := ReadConfig(path, testPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read a config file when we should have: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if used != testPath {
|
||||||
|
t.Fatalf("we read from the wrong file. got: %s, want: %s", used, testPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := AcmeDnsConfig{
|
||||||
|
General: general{
|
||||||
|
Listen: "127.0.0.1:53",
|
||||||
|
Proto: "both",
|
||||||
|
Domain: "test.example.org",
|
||||||
|
Nsname: "test.example.org",
|
||||||
|
Nsadmin: "test.example.org",
|
||||||
|
Debug: true,
|
||||||
|
StaticRecords: []string{
|
||||||
|
"test.example.org. A 127.0.0.1",
|
||||||
|
"test.example.org. NS test.example.org.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Database: dbsettings{
|
||||||
|
Engine: "dinosaur",
|
||||||
|
Connection: "roar",
|
||||||
|
},
|
||||||
|
API: httpapi{
|
||||||
|
Domain: "",
|
||||||
|
IP: "0.0.0.0",
|
||||||
|
DisableRegistration: false,
|
||||||
|
AutocertPort: "",
|
||||||
|
Port: "443",
|
||||||
|
TLS: "none",
|
||||||
|
TLSCertPrivkey: "/etc/tls/example.org/privkey.pem",
|
||||||
|
TLSCertFullchain: "/etc/tls/example.org/fullchain.pem",
|
||||||
|
ACMECacheDir: "api-certs",
|
||||||
|
NotificationEmail: "",
|
||||||
|
CorsOrigins: []string{"*"},
|
||||||
|
UseHeader: true,
|
||||||
|
HeaderName: "X-is-gonna-give-it-to-ya",
|
||||||
|
},
|
||||||
|
Logconfig: logconfig{
|
||||||
|
Level: "info",
|
||||||
|
Logtype: "stdout",
|
||||||
|
File: "./acme-dns.log",
|
||||||
|
Format: "json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(cfg, expected) {
|
||||||
|
t.Errorf("Did not read the config correctly: got %+v, want: %+v", cfg, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNonExistentPath() (string, error) {
|
||||||
|
path := fmt.Sprintf("/some/path/that/should/not/exist/on/any/filesystem/%10d.cfg", rand.Int())
|
||||||
|
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("attempted non existant file exists!?: %s", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestReadConfigFallbackError makes sure we error when we do not have a fallback config file
|
||||||
|
func TestReadConfigFallbackError(t *testing.T) {
|
||||||
|
var (
|
||||||
|
badPaths []string
|
||||||
|
i int
|
||||||
|
)
|
||||||
|
for len(badPaths) < 2 && i < 10 {
|
||||||
|
i++
|
||||||
|
|
||||||
|
if path, err := getNonExistentPath(); err == nil {
|
||||||
|
badPaths = append(badPaths, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(badPaths) != 2 {
|
||||||
|
t.Fatalf("did not create exactly 2 bad paths")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err := ReadConfig(badPaths[0], badPaths[1])
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Should have failed reading non existant file: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileCheckPermissionDenied(t *testing.T) {
|
||||||
|
tmpfile, err := os.CreateTemp("", "acmedns")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Could not create temporary file: %s", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tmpfile.Name())
|
||||||
|
_ = syscall.Chmod(tmpfile.Name(), 0000)
|
||||||
|
if FileIsAccessible(tmpfile.Name()) {
|
||||||
|
t.Errorf("File should not be accessible")
|
||||||
|
}
|
||||||
|
_ = syscall.Chmod(tmpfile.Name(), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileCheckNotExists(t *testing.T) {
|
||||||
|
if FileIsAccessible("/path/that/does/not/exist") {
|
||||||
|
t.Errorf("File should not be accessible")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileCheckOK(t *testing.T) {
|
||||||
|
tmpfile, err := os.CreateTemp("", "acmedns")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Could not create temporary file: %s", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tmpfile.Name())
|
||||||
|
if !FileIsAccessible(tmpfile.Name()) {
|
||||||
|
t.Errorf("File should be accessible")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrepareConfig(t *testing.T) {
|
||||||
|
for i, test := range []struct {
|
||||||
|
input AcmeDnsConfig
|
||||||
|
shoulderror bool
|
||||||
|
}{
|
||||||
|
{AcmeDnsConfig{
|
||||||
|
Database: dbsettings{Engine: "whatever", Connection: "whatever_too"},
|
||||||
|
API: httpapi{TLS: ApiTlsProviderNone},
|
||||||
|
}, false},
|
||||||
|
{AcmeDnsConfig{Database: dbsettings{Engine: "", Connection: "whatever_too"},
|
||||||
|
API: httpapi{TLS: ApiTlsProviderNone},
|
||||||
|
}, true},
|
||||||
|
{AcmeDnsConfig{Database: dbsettings{Engine: "whatever", Connection: ""},
|
||||||
|
API: httpapi{TLS: ApiTlsProviderNone},
|
||||||
|
}, true},
|
||||||
|
{AcmeDnsConfig{
|
||||||
|
Database: dbsettings{Engine: "whatever", Connection: "whatever_too"},
|
||||||
|
API: httpapi{TLS: "whatever"},
|
||||||
|
}, true},
|
||||||
|
} {
|
||||||
|
_, err := prepareConfig(test.input)
|
||||||
|
if test.shoulderror {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Test %d: Expected error with prepareConfig input data [%v]", i, test.input)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Test %d: Expected no error with prepareConfig input data [%v]", i, test.input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSanitizeString(t *testing.T) {
|
||||||
|
for i, test := range []struct {
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"abcd!abcd", "abcdabcd"},
|
||||||
|
{"ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz0123456789", "ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz0123456789"},
|
||||||
|
{"ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopq=@rstuvwxyz0123456789", "ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz0123456789"},
|
||||||
|
} {
|
||||||
|
if SanitizeString(test.input) != test.expected {
|
||||||
|
t.Errorf("Expected SanitizeString to return %s for test %d, but got %s instead", test.expected, i, SanitizeString(test.input))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCorrectPassword(t *testing.T) {
|
||||||
|
testPass, _ := bcrypt.GenerateFromPassword([]byte("nevergonnagiveyouup"), 10)
|
||||||
|
for i, test := range []struct {
|
||||||
|
input string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{"abcd", false},
|
||||||
|
{"nevergonnagiveyouup", true},
|
||||||
|
{"@rstuvwxyz0123456789", false},
|
||||||
|
} {
|
||||||
|
if test.expected && !CorrectPassword(test.input, string(testPass)) {
|
||||||
|
t.Errorf("Expected CorrectPassword to return %t for test %d", test.expected, i)
|
||||||
|
}
|
||||||
|
if !test.expected && CorrectPassword(test.input, string(testPass)) {
|
||||||
|
t.Errorf("Expected CorrectPassword to return %t for test %d", test.expected, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
135
pkg/api/api.go
Normal file
135
pkg/api/api.go
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/joohoi/acme-dns/pkg/acmedns"
|
||||||
|
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
"github.com/rs/cors"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AcmednsAPI struct {
|
||||||
|
Config *acmedns.AcmeDnsConfig
|
||||||
|
DB acmedns.AcmednsDB
|
||||||
|
Logger *zap.SugaredLogger
|
||||||
|
errChan chan error
|
||||||
|
}
|
||||||
|
|
||||||
|
func Init(config *acmedns.AcmeDnsConfig, db acmedns.AcmednsDB, logger *zap.SugaredLogger, errChan chan error) AcmednsAPI {
|
||||||
|
a := AcmednsAPI{Config: config, DB: db, Logger: logger, errChan: errChan}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AcmednsAPI) Start(dnsservers []acmedns.AcmednsNS) {
|
||||||
|
var err error
|
||||||
|
//TODO: do we want to debug log the HTTP server?
|
||||||
|
stderrorlog, err := zap.NewStdLogAt(a.Logger.Desugar(), zap.ErrorLevel)
|
||||||
|
if err != nil {
|
||||||
|
a.errChan <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
api := httprouter.New()
|
||||||
|
c := cors.New(cors.Options{
|
||||||
|
AllowedOrigins: a.Config.API.CorsOrigins,
|
||||||
|
AllowedMethods: []string{"GET", "POST"},
|
||||||
|
OptionsPassthrough: false,
|
||||||
|
Debug: a.Config.General.Debug,
|
||||||
|
})
|
||||||
|
if a.Config.General.Debug {
|
||||||
|
// Logwriter for saner log output
|
||||||
|
c.Log = stderrorlog
|
||||||
|
}
|
||||||
|
if !a.Config.API.DisableRegistration {
|
||||||
|
api.POST("/register", a.webRegisterPost)
|
||||||
|
}
|
||||||
|
api.POST("/update", a.Auth(a.webUpdatePost))
|
||||||
|
api.GET("/health", a.healthCheck)
|
||||||
|
|
||||||
|
host := a.Config.API.IP + ":" + a.Config.API.Port
|
||||||
|
|
||||||
|
// TLS specific general settings
|
||||||
|
cfg := &tls.Config{
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch a.Config.API.TLS {
|
||||||
|
case acmedns.ApiTlsProviderLetsEncrypt, acmedns.ApiTlsProviderLetsEncryptStaging:
|
||||||
|
magic := a.setupTLS(dnsservers)
|
||||||
|
err = magic.ManageAsync(context.Background(), []string{a.Config.General.Domain})
|
||||||
|
if err != nil {
|
||||||
|
a.errChan <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cfg.GetCertificate = magic.GetCertificate
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: host,
|
||||||
|
Handler: c.Handler(api),
|
||||||
|
TLSConfig: cfg,
|
||||||
|
ErrorLog: stderrorlog,
|
||||||
|
ReadTimeout: 5 * time.Second,
|
||||||
|
WriteTimeout: 10 * time.Second,
|
||||||
|
IdleTimeout: 120 * time.Second,
|
||||||
|
}
|
||||||
|
a.Logger.Infow("Listening HTTPS",
|
||||||
|
"host", host,
|
||||||
|
"domain", a.Config.General.Domain)
|
||||||
|
err = srv.ListenAndServeTLS("", "")
|
||||||
|
case acmedns.ApiTlsProviderCert:
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: host,
|
||||||
|
Handler: c.Handler(api),
|
||||||
|
TLSConfig: cfg,
|
||||||
|
ErrorLog: stderrorlog,
|
||||||
|
ReadTimeout: 5 * time.Second,
|
||||||
|
WriteTimeout: 10 * time.Second,
|
||||||
|
IdleTimeout: 120 * time.Second,
|
||||||
|
}
|
||||||
|
a.Logger.Infow("Listening HTTPS",
|
||||||
|
"host", host,
|
||||||
|
"domain", a.Config.General.Domain)
|
||||||
|
err = srv.ListenAndServeTLS(a.Config.API.TLSCertFullchain, a.Config.API.TLSCertPrivkey)
|
||||||
|
default:
|
||||||
|
a.Logger.Infow("Listening HTTP",
|
||||||
|
"host", host)
|
||||||
|
err = http.ListenAndServe(host, c.Handler(api))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
a.errChan <- err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AcmednsAPI) setupTLS(dnsservers []acmedns.AcmednsNS) *certmagic.Config {
|
||||||
|
provider := NewChallengeProvider(dnsservers)
|
||||||
|
certmagic.Default.Logger = a.Logger.Desugar()
|
||||||
|
storage := certmagic.FileStorage{Path: a.Config.API.ACMECacheDir}
|
||||||
|
|
||||||
|
// Set up certmagic for getting certificate for acme-dns api
|
||||||
|
certmagic.DefaultACME.DNS01Solver = &provider
|
||||||
|
certmagic.DefaultACME.Agreed = true
|
||||||
|
certmagic.DefaultACME.Logger = a.Logger.Desugar()
|
||||||
|
if a.Config.API.TLS == acmedns.ApiTlsProviderLetsEncrypt {
|
||||||
|
certmagic.DefaultACME.CA = certmagic.LetsEncryptProductionCA
|
||||||
|
} else {
|
||||||
|
certmagic.DefaultACME.CA = certmagic.LetsEncryptStagingCA
|
||||||
|
}
|
||||||
|
certmagic.DefaultACME.Email = a.Config.API.NotificationEmail
|
||||||
|
|
||||||
|
magicConf := certmagic.Default
|
||||||
|
magicConf.Logger = a.Logger.Desugar()
|
||||||
|
magicConf.Storage = &storage
|
||||||
|
magicConf.DefaultServerName = a.Config.General.Domain
|
||||||
|
magicCache := certmagic.NewCache(certmagic.CacheOptions{
|
||||||
|
GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) {
|
||||||
|
return &magicConf, nil
|
||||||
|
},
|
||||||
|
Logger: a.Logger.Desugar(),
|
||||||
|
})
|
||||||
|
magic := certmagic.New(magicCache, magicConf)
|
||||||
|
return magic
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package main
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@ -8,24 +8,38 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/joohoi/acme-dns/pkg/acmedns"
|
||||||
|
"github.com/joohoi/acme-dns/pkg/database"
|
||||||
|
"github.com/joohoi/acme-dns/pkg/nameserver"
|
||||||
|
|
||||||
|
"github.com/DATA-DOG/go-sqlmock"
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
"github.com/gavv/httpexpect"
|
"github.com/gavv/httpexpect"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
"github.com/rs/cors"
|
"github.com/rs/cors"
|
||||||
"github.com/satori/go.uuid"
|
"go.uber.org/zap"
|
||||||
"gopkg.in/DATA-DOG/go-sqlmock.v1"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func fakeConfigAndLogger() (acmedns.AcmeDnsConfig, *zap.SugaredLogger) {
|
||||||
|
c := acmedns.AcmeDnsConfig{}
|
||||||
|
c.Database.Engine = "sqlite"
|
||||||
|
c.Database.Connection = ":memory:"
|
||||||
|
l := zap.NewNop().Sugar()
|
||||||
|
return c, l
|
||||||
|
}
|
||||||
|
|
||||||
// noAuth function to write ACMETxt model to context while not preforming any validation
|
// noAuth function to write ACMETxt model to context while not preforming any validation
|
||||||
func noAuth(update httprouter.Handle) httprouter.Handle {
|
func noAuth(update httprouter.Handle) httprouter.Handle {
|
||||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||||
postData := ACMETxt{}
|
postData := acmedns.ACMETxt{}
|
||||||
uname := r.Header.Get("X-Api-User")
|
uname := r.Header.Get("X-Api-User")
|
||||||
passwd := r.Header.Get("X-Api-Key")
|
passwd := r.Header.Get("X-Api-Key")
|
||||||
|
|
||||||
dec := json.NewDecoder(r.Body)
|
dec := json.NewDecoder(r.Body)
|
||||||
_ = dec.Decode(&postData)
|
_ = dec.Decode(&postData)
|
||||||
// Set user info to the decoded ACMETxt object
|
// Set user info to the decoded ACMETxt object
|
||||||
postData.Username, _ = uuid.FromString(uname)
|
postData.Username, _ = uuid.Parse(uname)
|
||||||
postData.Password = passwd
|
postData.Password = passwd
|
||||||
// Set the ACMETxt struct to context to pull in from update function
|
// Set the ACMETxt struct to context to pull in from update function
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
@ -46,41 +60,37 @@ func getExpect(t *testing.T, server *httptest.Server) *httpexpect.Expect {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupRouter(debug bool, noauth bool) http.Handler {
|
func setupRouter(debug bool, noauth bool) (http.Handler, AcmednsAPI, acmedns.AcmednsDB) {
|
||||||
api := httprouter.New()
|
api := httprouter.New()
|
||||||
var dbcfg = dbsettings{
|
config, logger := fakeConfigAndLogger()
|
||||||
Engine: "sqlite3",
|
config.API.Domain = ""
|
||||||
Connection: ":memory:"}
|
config.API.Port = "8080"
|
||||||
var httpapicfg = httpapi{
|
config.API.TLS = acmedns.ApiTlsProviderNone
|
||||||
Domain: "",
|
config.API.CorsOrigins = []string{"*"}
|
||||||
Port: "8080",
|
config.API.UseHeader = true
|
||||||
TLS: "none",
|
config.API.HeaderName = "X-Forwarded-For"
|
||||||
CorsOrigins: []string{"*"},
|
|
||||||
UseHeader: true,
|
db, _ := database.Init(&config, logger)
|
||||||
HeaderName: "X-Forwarded-For",
|
errChan := make(chan error, 1)
|
||||||
}
|
adnsapi := Init(&config, db, logger, errChan)
|
||||||
var dnscfg = DNSConfig{
|
|
||||||
API: httpapicfg,
|
|
||||||
Database: dbcfg,
|
|
||||||
}
|
|
||||||
Config = dnscfg
|
|
||||||
c := cors.New(cors.Options{
|
c := cors.New(cors.Options{
|
||||||
AllowedOrigins: Config.API.CorsOrigins,
|
AllowedOrigins: config.API.CorsOrigins,
|
||||||
AllowedMethods: []string{"GET", "POST"},
|
AllowedMethods: []string{"GET", "POST"},
|
||||||
OptionsPassthrough: false,
|
OptionsPassthrough: false,
|
||||||
Debug: Config.General.Debug,
|
Debug: config.General.Debug,
|
||||||
})
|
})
|
||||||
api.POST("/register", webRegisterPost)
|
api.POST("/register", adnsapi.webRegisterPost)
|
||||||
|
api.GET("/health", adnsapi.healthCheck)
|
||||||
if noauth {
|
if noauth {
|
||||||
api.POST("/update", noAuth(webUpdatePost))
|
api.POST("/update", noAuth(adnsapi.webUpdatePost))
|
||||||
} else {
|
} else {
|
||||||
api.POST("/update", Auth(webUpdatePost))
|
api.POST("/update", adnsapi.Auth(adnsapi.webUpdatePost))
|
||||||
}
|
}
|
||||||
return c.Handler(api)
|
return c.Handler(api), adnsapi, db
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestApiRegister(t *testing.T) {
|
func TestApiRegister(t *testing.T) {
|
||||||
router := setupRouter(false, false)
|
router, _, _ := setupRouter(false, false)
|
||||||
server := httptest.NewServer(router)
|
server := httptest.NewServer(router)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
e := getExpect(t, server)
|
e := getExpect(t, server)
|
||||||
@ -94,9 +104,10 @@ func TestApiRegister(t *testing.T) {
|
|||||||
NotContainsKey("error")
|
NotContainsKey("error")
|
||||||
|
|
||||||
allowfrom := map[string][]interface{}{
|
allowfrom := map[string][]interface{}{
|
||||||
"allowfrom": []interface{}{"123.123.123.123/32",
|
"allowfrom": {"123.123.123.123/32",
|
||||||
"1010.10.10.10/24",
|
"2001:db8:a0b:12f0::1/32",
|
||||||
"invalid"},
|
"[::1]/64",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
response := e.POST("/register").
|
response := e.POST("/register").
|
||||||
@ -111,11 +122,41 @@ func TestApiRegister(t *testing.T) {
|
|||||||
ContainsKey("allowfrom").
|
ContainsKey("allowfrom").
|
||||||
NotContainsKey("error")
|
NotContainsKey("error")
|
||||||
|
|
||||||
response.Value("allowfrom").Array().Elements("123.123.123.123/32")
|
response.Value("allowfrom").Array().Elements("123.123.123.123/32", "2001:db8:a0b:12f0::1/32", "::1/64")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApiRegisterBadAllowFrom(t *testing.T) {
|
||||||
|
router, _, _ := setupRouter(false, false)
|
||||||
|
server := httptest.NewServer(router)
|
||||||
|
defer server.Close()
|
||||||
|
e := getExpect(t, server)
|
||||||
|
invalidVals := []string{
|
||||||
|
"invalid",
|
||||||
|
"1.2.3.4/33",
|
||||||
|
"1.2/24",
|
||||||
|
"1.2.3.4",
|
||||||
|
"12345:db8:a0b:12f0::1/32",
|
||||||
|
"1234::123::123::1/32",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range invalidVals {
|
||||||
|
|
||||||
|
allowfrom := map[string][]interface{}{
|
||||||
|
"allowfrom": {v}}
|
||||||
|
|
||||||
|
response := e.POST("/register").
|
||||||
|
WithJSON(allowfrom).
|
||||||
|
Expect().
|
||||||
|
Status(http.StatusBadRequest).
|
||||||
|
JSON().Object().
|
||||||
|
ContainsKey("error")
|
||||||
|
|
||||||
|
response.Value("error").Equal("invalid_allowfrom_cidr")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestApiRegisterMalformedJSON(t *testing.T) {
|
func TestApiRegisterMalformedJSON(t *testing.T) {
|
||||||
router := setupRouter(false, false)
|
router, _, _ := setupRouter(false, false)
|
||||||
server := httptest.NewServer(router)
|
server := httptest.NewServer(router)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
e := getExpect(t, server)
|
e := getExpect(t, server)
|
||||||
@ -142,13 +183,13 @@ func TestApiRegisterMalformedJSON(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestApiRegisterWithMockDB(t *testing.T) {
|
func TestApiRegisterWithMockDB(t *testing.T) {
|
||||||
router := setupRouter(false, false)
|
router, _, db := setupRouter(false, false)
|
||||||
server := httptest.NewServer(router)
|
server := httptest.NewServer(router)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
e := getExpect(t, server)
|
e := getExpect(t, server)
|
||||||
oldDb := DB.GetBackend()
|
oldDb := db.GetBackend()
|
||||||
db, mock, _ := sqlmock.New()
|
mdb, mock, _ := sqlmock.New()
|
||||||
DB.SetBackend(db)
|
db.SetBackend(mdb)
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
mock.ExpectBegin()
|
mock.ExpectBegin()
|
||||||
mock.ExpectPrepare("INSERT INTO records").WillReturnError(errors.New("error"))
|
mock.ExpectPrepare("INSERT INTO records").WillReturnError(errors.New("error"))
|
||||||
@ -156,11 +197,71 @@ func TestApiRegisterWithMockDB(t *testing.T) {
|
|||||||
Status(http.StatusInternalServerError).
|
Status(http.StatusInternalServerError).
|
||||||
JSON().Object().
|
JSON().Object().
|
||||||
ContainsKey("error")
|
ContainsKey("error")
|
||||||
DB.SetBackend(oldDb)
|
db.SetBackend(oldDb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApiUpdateWithInvalidSubdomain(t *testing.T) {
|
||||||
|
validTxtData := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
|
||||||
|
updateJSON := map[string]interface{}{
|
||||||
|
"subdomain": "",
|
||||||
|
"txt": ""}
|
||||||
|
|
||||||
|
router, _, db := setupRouter(false, false)
|
||||||
|
server := httptest.NewServer(router)
|
||||||
|
defer server.Close()
|
||||||
|
e := getExpect(t, server)
|
||||||
|
newUser, err := db.Register(acmedns.Cidrslice{})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Could not create new user, got error [%v]", err)
|
||||||
|
}
|
||||||
|
// Invalid subdomain data
|
||||||
|
updateJSON["subdomain"] = "example.com"
|
||||||
|
updateJSON["txt"] = validTxtData
|
||||||
|
e.POST("/update").
|
||||||
|
WithJSON(updateJSON).
|
||||||
|
WithHeader("X-Api-User", newUser.Username.String()).
|
||||||
|
WithHeader("X-Api-Key", newUser.Password).
|
||||||
|
Expect().
|
||||||
|
Status(http.StatusUnauthorized).
|
||||||
|
JSON().Object().
|
||||||
|
ContainsKey("error").
|
||||||
|
NotContainsKey("txt").
|
||||||
|
ValueEqual("error", "forbidden")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApiUpdateWithInvalidTxt(t *testing.T) {
|
||||||
|
invalidTXTData := "idk m8 bbl lmao"
|
||||||
|
|
||||||
|
updateJSON := map[string]interface{}{
|
||||||
|
"subdomain": "",
|
||||||
|
"txt": ""}
|
||||||
|
|
||||||
|
router, _, db := setupRouter(false, false)
|
||||||
|
server := httptest.NewServer(router)
|
||||||
|
defer server.Close()
|
||||||
|
e := getExpect(t, server)
|
||||||
|
newUser, err := db.Register(acmedns.Cidrslice{})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Could not create new user, got error [%v]", err)
|
||||||
|
}
|
||||||
|
updateJSON["subdomain"] = newUser.Subdomain
|
||||||
|
// Invalid txt data
|
||||||
|
updateJSON["txt"] = invalidTXTData
|
||||||
|
e.POST("/update").
|
||||||
|
WithJSON(updateJSON).
|
||||||
|
WithHeader("X-Api-User", newUser.Username.String()).
|
||||||
|
WithHeader("X-Api-Key", newUser.Password).
|
||||||
|
Expect().
|
||||||
|
Status(http.StatusBadRequest).
|
||||||
|
JSON().Object().
|
||||||
|
ContainsKey("error").
|
||||||
|
NotContainsKey("txt").
|
||||||
|
ValueEqual("error", "bad_txt")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestApiUpdateWithoutCredentials(t *testing.T) {
|
func TestApiUpdateWithoutCredentials(t *testing.T) {
|
||||||
router := setupRouter(false, false)
|
router, _, _ := setupRouter(false, false)
|
||||||
server := httptest.NewServer(router)
|
server := httptest.NewServer(router)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
e := getExpect(t, server)
|
e := getExpect(t, server)
|
||||||
@ -178,11 +279,11 @@ func TestApiUpdateWithCredentials(t *testing.T) {
|
|||||||
"subdomain": "",
|
"subdomain": "",
|
||||||
"txt": ""}
|
"txt": ""}
|
||||||
|
|
||||||
router := setupRouter(false, false)
|
router, _, db := setupRouter(false, false)
|
||||||
server := httptest.NewServer(router)
|
server := httptest.NewServer(router)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
e := getExpect(t, server)
|
e := getExpect(t, server)
|
||||||
newUser, err := DB.Register(cidrslice{})
|
newUser, err := db.Register(acmedns.Cidrslice{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Could not create new user, got error [%v]", err)
|
t.Errorf("Could not create new user, got error [%v]", err)
|
||||||
}
|
}
|
||||||
@ -211,13 +312,13 @@ func TestApiUpdateWithCredentialsMockDB(t *testing.T) {
|
|||||||
updateJSON["subdomain"] = "a097455b-52cc-4569-90c8-7a4b97c6eba8"
|
updateJSON["subdomain"] = "a097455b-52cc-4569-90c8-7a4b97c6eba8"
|
||||||
updateJSON["txt"] = validTxtData
|
updateJSON["txt"] = validTxtData
|
||||||
|
|
||||||
router := setupRouter(false, true)
|
router, _, db := setupRouter(false, true)
|
||||||
server := httptest.NewServer(router)
|
server := httptest.NewServer(router)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
e := getExpect(t, server)
|
e := getExpect(t, server)
|
||||||
oldDb := DB.GetBackend()
|
oldDb := db.GetBackend()
|
||||||
db, mock, _ := sqlmock.New()
|
mdb, mock, _ := sqlmock.New()
|
||||||
DB.SetBackend(db)
|
db.SetBackend(mdb)
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
mock.ExpectBegin()
|
mock.ExpectBegin()
|
||||||
mock.ExpectPrepare("UPDATE records").WillReturnError(errors.New("error"))
|
mock.ExpectPrepare("UPDATE records").WillReturnError(errors.New("error"))
|
||||||
@ -227,35 +328,31 @@ func TestApiUpdateWithCredentialsMockDB(t *testing.T) {
|
|||||||
Status(http.StatusInternalServerError).
|
Status(http.StatusInternalServerError).
|
||||||
JSON().Object().
|
JSON().Object().
|
||||||
ContainsKey("error")
|
ContainsKey("error")
|
||||||
DB.SetBackend(oldDb)
|
db.SetBackend(oldDb)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestApiManyUpdateWithCredentials(t *testing.T) {
|
func TestApiManyUpdateWithCredentials(t *testing.T) {
|
||||||
validTxtData := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
validTxtData := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
|
||||||
updateJSON := map[string]interface{}{
|
router, _, db := setupRouter(true, false)
|
||||||
"subdomain": "",
|
|
||||||
"txt": ""}
|
|
||||||
|
|
||||||
router := setupRouter(true, false)
|
|
||||||
server := httptest.NewServer(router)
|
server := httptest.NewServer(router)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
e := getExpect(t, server)
|
e := getExpect(t, server)
|
||||||
// User without defined CIDR masks
|
// User without defined CIDR masks
|
||||||
newUser, err := DB.Register(cidrslice{})
|
newUser, err := db.Register(acmedns.Cidrslice{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Could not create new user, got error [%v]", err)
|
t.Errorf("Could not create new user, got error [%v]", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// User with defined allow from - CIDR masks, all invalid
|
// User with defined allow from - CIDR masks, all invalid
|
||||||
// (httpexpect doesn't provide a way to mock remote ip)
|
// (httpexpect doesn't provide a way to mock remote ip)
|
||||||
newUserWithCIDR, err := DB.Register(cidrslice{"192.168.1.1/32", "invalid"})
|
newUserWithCIDR, err := db.Register(acmedns.Cidrslice{"192.168.1.1/32", "invalid"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Could not create new user with CIDR, got error [%v]", err)
|
t.Errorf("Could not create new user with CIDR, got error [%v]", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Another user with valid CIDR mask to match the httpexpect default
|
// Another user with valid CIDR mask to match the httpexpect default
|
||||||
newUserWithValidCIDR, err := DB.Register(cidrslice{"10.1.2.3/32", "invalid"})
|
newUserWithValidCIDR, err := db.Register(acmedns.Cidrslice{"10.1.2.3/32", "invalid"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Could not create new user with a valid CIDR, got error [%v]", err)
|
t.Errorf("Could not create new user with a valid CIDR, got error [%v]", err)
|
||||||
}
|
}
|
||||||
@ -278,7 +375,7 @@ func TestApiManyUpdateWithCredentials(t *testing.T) {
|
|||||||
{newUserWithValidCIDR.Username.String(), newUserWithValidCIDR.Password, newUserWithValidCIDR.Subdomain, validTxtData, 200},
|
{newUserWithValidCIDR.Username.String(), newUserWithValidCIDR.Password, newUserWithValidCIDR.Subdomain, validTxtData, 200},
|
||||||
{newUser.Username.String(), "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", newUser.Subdomain, validTxtData, 401},
|
{newUser.Username.String(), "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", newUser.Subdomain, validTxtData, 401},
|
||||||
} {
|
} {
|
||||||
updateJSON = map[string]interface{}{
|
updateJSON := map[string]interface{}{
|
||||||
"subdomain": test.subdomain,
|
"subdomain": test.subdomain,
|
||||||
"txt": test.txt}
|
"txt": test.txt}
|
||||||
e.POST("/update").
|
e.POST("/update").
|
||||||
@ -293,34 +390,30 @@ func TestApiManyUpdateWithCredentials(t *testing.T) {
|
|||||||
|
|
||||||
func TestApiManyUpdateWithIpCheckHeaders(t *testing.T) {
|
func TestApiManyUpdateWithIpCheckHeaders(t *testing.T) {
|
||||||
|
|
||||||
updateJSON := map[string]interface{}{
|
router, adnsapi, db := setupRouter(false, false)
|
||||||
"subdomain": "",
|
|
||||||
"txt": ""}
|
|
||||||
|
|
||||||
router := setupRouter(false, false)
|
|
||||||
server := httptest.NewServer(router)
|
server := httptest.NewServer(router)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
e := getExpect(t, server)
|
e := getExpect(t, server)
|
||||||
// Use header checks from default header (X-Forwarded-For)
|
// Use header checks from default header (X-Forwarded-For)
|
||||||
Config.API.UseHeader = true
|
adnsapi.Config.API.UseHeader = true
|
||||||
// User without defined CIDR masks
|
// User without defined CIDR masks
|
||||||
newUser, err := DB.Register(cidrslice{})
|
newUser, err := db.Register(acmedns.Cidrslice{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Could not create new user, got error [%v]", err)
|
t.Errorf("Could not create new user, got error [%v]", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
newUserWithCIDR, err := DB.Register(cidrslice{"192.168.1.2/32", "invalid"})
|
newUserWithCIDR, err := db.Register(acmedns.Cidrslice{"192.168.1.2/32", "invalid"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Could not create new user with CIDR, got error [%v]", err)
|
t.Errorf("Could not create new user with CIDR, got error [%v]", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
newUserWithIP6CIDR, err := DB.Register(cidrslice{"2002:c0a8::0/32"})
|
newUserWithIP6CIDR, err := db.Register(acmedns.Cidrslice{"2002:c0a8::0/32"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Could not create a new user with IP6 CIDR, got error [%v]", err)
|
t.Errorf("Could not create a new user with IP6 CIDR, got error [%v]", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range []struct {
|
for _, test := range []struct {
|
||||||
user ACMETxt
|
user acmedns.ACMETxt
|
||||||
headerValue string
|
headerValue string
|
||||||
status int
|
status int
|
||||||
}{
|
}{
|
||||||
@ -333,7 +426,7 @@ func TestApiManyUpdateWithIpCheckHeaders(t *testing.T) {
|
|||||||
{newUserWithIP6CIDR, "2002:c0a7:0ff::0", 401},
|
{newUserWithIP6CIDR, "2002:c0a7:0ff::0", 401},
|
||||||
{newUserWithIP6CIDR, "2002:c0a8:d3ad:b33f:c0ff:33b4:dc0d:3b4d", 200},
|
{newUserWithIP6CIDR, "2002:c0a8:d3ad:b33f:c0ff:33b4:dc0d:3b4d", 200},
|
||||||
} {
|
} {
|
||||||
updateJSON = map[string]interface{}{
|
updateJSON := map[string]interface{}{
|
||||||
"subdomain": test.user.Subdomain,
|
"subdomain": test.user.Subdomain,
|
||||||
"txt": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}
|
"txt": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}
|
||||||
e.POST("/update").
|
e.POST("/update").
|
||||||
@ -344,5 +437,97 @@ func TestApiManyUpdateWithIpCheckHeaders(t *testing.T) {
|
|||||||
Expect().
|
Expect().
|
||||||
Status(test.status)
|
Status(test.status)
|
||||||
}
|
}
|
||||||
Config.API.UseHeader = false
|
adnsapi.Config.API.UseHeader = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApiHealthCheck(t *testing.T) {
|
||||||
|
router, _, _ := setupRouter(false, false)
|
||||||
|
server := httptest.NewServer(router)
|
||||||
|
defer server.Close()
|
||||||
|
e := getExpect(t, server)
|
||||||
|
e.GET("/health").Expect().Status(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetIPListFromHeader(t *testing.T) {
|
||||||
|
for i, test := range []struct {
|
||||||
|
input string
|
||||||
|
output []string
|
||||||
|
}{
|
||||||
|
{"1.1.1.1, 2.2.2.2", []string{"1.1.1.1", "2.2.2.2"}},
|
||||||
|
{" 1.1.1.1 , 2.2.2.2", []string{"1.1.1.1", "2.2.2.2"}},
|
||||||
|
{",1.1.1.1 ,2.2.2.2", []string{"1.1.1.1", "2.2.2.2"}},
|
||||||
|
} {
|
||||||
|
res := getIPListFromHeader(test.input)
|
||||||
|
if len(res) != len(test.output) {
|
||||||
|
t.Errorf("Test %d: Expected [%d] items in return list, but got [%d]", i, len(test.output), len(res))
|
||||||
|
} else {
|
||||||
|
|
||||||
|
for j, vv := range test.output {
|
||||||
|
if res[j] != vv {
|
||||||
|
t.Errorf("Test %d: Expected return value [%v] but got [%v]", j, test.output, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateAllowedFromIP(t *testing.T) {
|
||||||
|
_, adnsapi, _ := setupRouter(false, false)
|
||||||
|
adnsapi.Config.API.UseHeader = false
|
||||||
|
userWithAllow := acmedns.NewACMETxt()
|
||||||
|
userWithAllow.AllowFrom = acmedns.Cidrslice{"192.168.1.2/32", "[::1]/128"}
|
||||||
|
userWithoutAllow := acmedns.NewACMETxt()
|
||||||
|
|
||||||
|
for i, test := range []struct {
|
||||||
|
remoteaddr string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{"192.168.1.2:1234", true},
|
||||||
|
{"192.168.1.1:1234", false},
|
||||||
|
{"invalid", false},
|
||||||
|
{"[::1]:4567", true},
|
||||||
|
} {
|
||||||
|
newreq, _ := http.NewRequest("GET", "/whatever", nil)
|
||||||
|
newreq.RemoteAddr = test.remoteaddr
|
||||||
|
ret := adnsapi.updateAllowedFromIP(newreq, userWithAllow)
|
||||||
|
if test.expected != ret {
|
||||||
|
t.Errorf("Test %d: Unexpected result for user with allowForm set", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !adnsapi.updateAllowedFromIP(newreq, userWithoutAllow) {
|
||||||
|
t.Errorf("Test %d: Unexpected result for user without allowForm set", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetupTLS(t *testing.T) {
|
||||||
|
_, svr, _ := setupRouter(false, false)
|
||||||
|
|
||||||
|
for _, test := range []struct {
|
||||||
|
apiTls string
|
||||||
|
expectedCA string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
apiTls: acmedns.ApiTlsProviderLetsEncrypt,
|
||||||
|
expectedCA: certmagic.LetsEncryptProductionCA,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apiTls: acmedns.ApiTlsProviderLetsEncryptStaging,
|
||||||
|
expectedCA: certmagic.LetsEncryptStagingCA,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
svr.Config.API.TLS = test.apiTls
|
||||||
|
ns := &nameserver.Nameserver{}
|
||||||
|
magic := svr.setupTLS([]acmedns.AcmednsNS{ns})
|
||||||
|
|
||||||
|
if test.expectedCA != certmagic.DefaultACME.CA {
|
||||||
|
t.Errorf("failed to configure default ACME CA. got %s, want %s", certmagic.DefaultACME.CA, test.expectedCA)
|
||||||
|
}
|
||||||
|
|
||||||
|
if magic.DefaultServerName != svr.Config.General.Domain {
|
||||||
|
t.Errorf("failed to set the correct doman. got: %s, want %s", magic.DefaultServerName, svr.Config.General.Domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
103
pkg/api/auth.go
Normal file
103
pkg/api/auth.go
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
|
||||||
|
"github.com/joohoi/acme-dns/pkg/acmedns"
|
||||||
|
)
|
||||||
|
|
||||||
|
type key int
|
||||||
|
|
||||||
|
// ACMETxtKey is a context key for ACMETxt struct
|
||||||
|
const ACMETxtKey key = 0
|
||||||
|
|
||||||
|
// Auth middleware for update request
|
||||||
|
func (a *AcmednsAPI) Auth(update httprouter.Handle) httprouter.Handle {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||||
|
postData := acmedns.ACMETxt{}
|
||||||
|
userOK := false
|
||||||
|
user, err := a.getUserFromRequest(r)
|
||||||
|
if err == nil {
|
||||||
|
if a.updateAllowedFromIP(r, user) {
|
||||||
|
dec := json.NewDecoder(r.Body)
|
||||||
|
err = dec.Decode(&postData)
|
||||||
|
if err != nil {
|
||||||
|
a.Logger.Errorw("Decoding error",
|
||||||
|
"error", "json_error")
|
||||||
|
}
|
||||||
|
if user.Subdomain == postData.Subdomain {
|
||||||
|
userOK = true
|
||||||
|
} else {
|
||||||
|
a.Logger.Errorw("Subdomain mismatch",
|
||||||
|
"error", "subdomain_mismatch",
|
||||||
|
"name", postData.Subdomain,
|
||||||
|
"expected", user.Subdomain)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
a.Logger.Errorw("Update not allowed from IP",
|
||||||
|
"error", "ip_unauthorized")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
a.Logger.Errorw("Error while trying to get user",
|
||||||
|
"error", err.Error())
|
||||||
|
}
|
||||||
|
if userOK {
|
||||||
|
// Set user info to the decoded ACMETxt object
|
||||||
|
postData.Username = user.Username
|
||||||
|
postData.Password = user.Password
|
||||||
|
// Set the ACMETxt struct to context to pull in from update function
|
||||||
|
ctx := context.WithValue(r.Context(), ACMETxtKey, postData)
|
||||||
|
update(w, r.WithContext(ctx), p)
|
||||||
|
} else {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
_, _ = w.Write(jsonError("forbidden"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AcmednsAPI) getUserFromRequest(r *http.Request) (acmedns.ACMETxt, error) {
|
||||||
|
uname := r.Header.Get("X-Api-User")
|
||||||
|
passwd := r.Header.Get("X-Api-Key")
|
||||||
|
username, err := getValidUsername(uname)
|
||||||
|
if err != nil {
|
||||||
|
return acmedns.ACMETxt{}, fmt.Errorf("invalid username: %s: %w", uname, err)
|
||||||
|
}
|
||||||
|
if validKey(passwd) {
|
||||||
|
dbuser, err := a.DB.GetByUsername(username)
|
||||||
|
if err != nil {
|
||||||
|
a.Logger.Errorw("Error while trying to get user",
|
||||||
|
"error", err.Error())
|
||||||
|
// To protect against timed side channel (never gonna give you up)
|
||||||
|
acmedns.CorrectPassword(passwd, "$2a$10$8JEFVNYYhLoBysjAxe2yBuXrkDojBQBkVpXEQgyQyjn43SvJ4vL36")
|
||||||
|
|
||||||
|
return acmedns.ACMETxt{}, fmt.Errorf("invalid username: %s", uname)
|
||||||
|
}
|
||||||
|
if acmedns.CorrectPassword(passwd, dbuser.Password) {
|
||||||
|
return dbuser, nil
|
||||||
|
}
|
||||||
|
return acmedns.ACMETxt{}, fmt.Errorf("invalid password for user %s", uname)
|
||||||
|
}
|
||||||
|
return acmedns.ACMETxt{}, fmt.Errorf("invalid key for user %s", uname)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AcmednsAPI) updateAllowedFromIP(r *http.Request, user acmedns.ACMETxt) bool {
|
||||||
|
if a.Config.API.UseHeader {
|
||||||
|
ips := getIPListFromHeader(r.Header.Get(a.Config.API.HeaderName))
|
||||||
|
return user.AllowedFromList(ips)
|
||||||
|
}
|
||||||
|
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
a.Logger.Errorw("Error while parsing remote address",
|
||||||
|
"error", err.Error(),
|
||||||
|
"remoteaddr", r.RemoteAddr)
|
||||||
|
host = ""
|
||||||
|
}
|
||||||
|
return user.AllowedFrom(host)
|
||||||
|
}
|
||||||
40
pkg/api/challengeprovider.go
Normal file
40
pkg/api/challengeprovider.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/mholt/acmez/v3/acme"
|
||||||
|
|
||||||
|
"github.com/joohoi/acme-dns/pkg/acmedns"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ChallengeProvider implements go-acme/lego Provider interface which is used for ACME DNS challenge handling
|
||||||
|
type ChallengeProvider struct {
|
||||||
|
servers []acmedns.AcmednsNS
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewChallengeProvider creates a new instance of ChallengeProvider
|
||||||
|
func NewChallengeProvider(servers []acmedns.AcmednsNS) ChallengeProvider {
|
||||||
|
return ChallengeProvider{servers: servers}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Present is used for making the ACME DNS challenge token available for DNS
|
||||||
|
func (c *ChallengeProvider) Present(ctx context.Context, challenge acme.Challenge) error {
|
||||||
|
for _, s := range c.servers {
|
||||||
|
s.SetOwnAuthKey(challenge.DNS01KeyAuthorization())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanUp is called after the run to remove the ACME DNS challenge tokens from DNS records
|
||||||
|
func (c *ChallengeProvider) CleanUp(ctx context.Context, _ acme.Challenge) error {
|
||||||
|
for _, s := range c.servers {
|
||||||
|
s.SetOwnAuthKey("")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait is a dummy function as we are just going to be ready to answer the challenge from the get-go
|
||||||
|
func (c *ChallengeProvider) Wait(_ context.Context, _ acme.Challenge) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
56
pkg/api/challengeprovider_test.go
Normal file
56
pkg/api/challengeprovider_test.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/joohoi/acme-dns/pkg/acmedns"
|
||||||
|
"github.com/mholt/acmez/v3/acme"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockNameserver struct {
|
||||||
|
acmedns.AcmednsNS
|
||||||
|
authKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockNameserver) SetOwnAuthKey(key string) {
|
||||||
|
m.authKey = key
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChallengeProvider(t *testing.T) {
|
||||||
|
mock := &mockNameserver{}
|
||||||
|
servers := []acmedns.AcmednsNS{mock}
|
||||||
|
cp := NewChallengeProvider(servers)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
challenge := acme.Challenge{
|
||||||
|
Type: "dns-01",
|
||||||
|
Token: "test-token",
|
||||||
|
KeyAuthorization: "test-key-auth",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Present
|
||||||
|
err := cp.Present(ctx, challenge)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Present failed: %v", err)
|
||||||
|
}
|
||||||
|
expectedKey := challenge.DNS01KeyAuthorization()
|
||||||
|
if mock.authKey != expectedKey {
|
||||||
|
t.Errorf("Expected auth key %s, got %s", expectedKey, mock.authKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test CleanUp
|
||||||
|
err = cp.CleanUp(ctx, challenge)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("CleanUp failed: %v", err)
|
||||||
|
}
|
||||||
|
if mock.authKey != "" {
|
||||||
|
t.Errorf("Expected empty auth key after CleanUp, got %s", mock.authKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Wait
|
||||||
|
err = cp.Wait(ctx, challenge)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Wait failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
12
pkg/api/healthcheck.go
Normal file
12
pkg/api/healthcheck.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Endpoint used to check the readiness and/or liveness (health) of the server.
|
||||||
|
func (a *AcmednsAPI) healthCheck(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
76
pkg/api/register.go
Normal file
76
pkg/api/register.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
|
||||||
|
"github.com/joohoi/acme-dns/pkg/acmedns"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegResponse is a struct for registration response JSON
|
||||||
|
type RegResponse struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Fulldomain string `json:"fulldomain"`
|
||||||
|
Subdomain string `json:"subdomain"`
|
||||||
|
Allowfrom []string `json:"allowfrom"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AcmednsAPI) webRegisterPost(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||||
|
var regStatus int
|
||||||
|
var reg []byte
|
||||||
|
var err error
|
||||||
|
aTXT := acmedns.ACMETxt{}
|
||||||
|
bdata, _ := io.ReadAll(r.Body)
|
||||||
|
if len(bdata) > 0 {
|
||||||
|
err = json.Unmarshal(bdata, &aTXT)
|
||||||
|
if err != nil {
|
||||||
|
regStatus = http.StatusBadRequest
|
||||||
|
reg = jsonError("malformed_json_payload")
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(regStatus)
|
||||||
|
_, _ = w.Write(reg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fail with malformed CIDR mask in allowfrom
|
||||||
|
err = aTXT.AllowFrom.IsValid()
|
||||||
|
if err != nil {
|
||||||
|
regStatus = http.StatusBadRequest
|
||||||
|
reg = jsonError("invalid_allowfrom_cidr")
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(regStatus)
|
||||||
|
_, _ = w.Write(reg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new user
|
||||||
|
nu, err := a.DB.Register(aTXT.AllowFrom)
|
||||||
|
if err != nil {
|
||||||
|
errstr := fmt.Sprintf("%v", err)
|
||||||
|
reg = jsonError(errstr)
|
||||||
|
regStatus = http.StatusInternalServerError
|
||||||
|
a.Logger.Errorw("Error in registration",
|
||||||
|
"error", err.Error())
|
||||||
|
} else {
|
||||||
|
a.Logger.Debugw("Created new user",
|
||||||
|
"user", nu.Username.String())
|
||||||
|
regStruct := RegResponse{nu.Username.String(), nu.Password, nu.Subdomain + "." + a.Config.General.Domain, nu.Subdomain, nu.AllowFrom.ValidEntries()}
|
||||||
|
regStatus = http.StatusCreated
|
||||||
|
reg, err = json.Marshal(regStruct)
|
||||||
|
if err != nil {
|
||||||
|
regStatus = http.StatusInternalServerError
|
||||||
|
reg = jsonError("json_error")
|
||||||
|
a.Logger.Errorw("Could not marshal JSON",
|
||||||
|
"error", "json")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(regStatus)
|
||||||
|
_, _ = w.Write(reg)
|
||||||
|
}
|
||||||
55
pkg/api/update.go
Normal file
55
pkg/api/update.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/joohoi/acme-dns/pkg/acmedns"
|
||||||
|
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *AcmednsAPI) webUpdatePost(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||||
|
var updStatus int
|
||||||
|
var upd []byte
|
||||||
|
// Get user
|
||||||
|
atxt, ok := r.Context().Value(ACMETxtKey).(acmedns.ACMETxt)
|
||||||
|
if !ok {
|
||||||
|
a.Logger.Errorw("Context error",
|
||||||
|
"error", "context")
|
||||||
|
}
|
||||||
|
// NOTE: An invalid subdomain should not happen - the auth handler should
|
||||||
|
// reject POSTs with an invalid subdomain before this handler. Reject any
|
||||||
|
// invalid subdomains anyway as a matter of caution.
|
||||||
|
if !validSubdomain(atxt.Subdomain) {
|
||||||
|
a.Logger.Errorw("Bad update data",
|
||||||
|
"error", "subdomain",
|
||||||
|
"subdomain", atxt.Subdomain,
|
||||||
|
"txt", atxt.Value)
|
||||||
|
updStatus = http.StatusBadRequest
|
||||||
|
upd = jsonError("bad_subdomain")
|
||||||
|
} else if !validTXT(atxt.Value) {
|
||||||
|
a.Logger.Errorw("Bad update data",
|
||||||
|
"error", "txt",
|
||||||
|
"subdomain", atxt.Subdomain,
|
||||||
|
"txt", atxt.Value)
|
||||||
|
updStatus = http.StatusBadRequest
|
||||||
|
upd = jsonError("bad_txt")
|
||||||
|
} else if validSubdomain(atxt.Subdomain) && validTXT(atxt.Value) {
|
||||||
|
err := a.DB.Update(atxt.ACMETxtPost)
|
||||||
|
if err != nil {
|
||||||
|
a.Logger.Errorw("Error while trying to update record",
|
||||||
|
"error", err.Error())
|
||||||
|
updStatus = http.StatusInternalServerError
|
||||||
|
upd = jsonError("db_error")
|
||||||
|
} else {
|
||||||
|
a.Logger.Debugw("TXT record updated",
|
||||||
|
"subdomain", atxt.Subdomain,
|
||||||
|
"txt", atxt.Value)
|
||||||
|
updStatus = http.StatusOK
|
||||||
|
upd = []byte("{\"txt\": \"" + atxt.Value + "\"}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(updStatus)
|
||||||
|
_, _ = w.Write(upd)
|
||||||
|
}
|
||||||
59
pkg/api/util.go
Normal file
59
pkg/api/util.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
|
"github.com/joohoi/acme-dns/pkg/acmedns"
|
||||||
|
)
|
||||||
|
|
||||||
|
func jsonError(message string) []byte {
|
||||||
|
return []byte(fmt.Sprintf("{\"error\": \"%s\"}", message))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getValidUsername(u string) (uuid.UUID, error) {
|
||||||
|
uname, err := uuid.Parse(u)
|
||||||
|
if err != nil {
|
||||||
|
return uuid.UUID{}, err
|
||||||
|
}
|
||||||
|
return uname, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validKey(k string) bool {
|
||||||
|
kn := acmedns.SanitizeString(k)
|
||||||
|
if utf8.RuneCountInString(k) == 40 && utf8.RuneCountInString(kn) == 40 {
|
||||||
|
// Correct length and all chars valid
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIPListFromHeader(header string) []string {
|
||||||
|
iplist := []string{}
|
||||||
|
for _, v := range strings.Split(header, ",") {
|
||||||
|
if len(v) > 0 {
|
||||||
|
// Ignore empty values
|
||||||
|
iplist = append(iplist, strings.TrimSpace(v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return iplist
|
||||||
|
}
|
||||||
|
|
||||||
|
func validSubdomain(s string) bool {
|
||||||
|
// URL safe base64 alphabet without padding as defined in ACME
|
||||||
|
RegExp := regexp.MustCompile("^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$")
|
||||||
|
return RegExp.MatchString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validTXT(s string) bool {
|
||||||
|
sn := acmedns.SanitizeString(s)
|
||||||
|
if utf8.RuneCountInString(s) == 43 && utf8.RuneCountInString(sn) == 43 {
|
||||||
|
// 43 chars is the current LE auth key size, but not limited / defined by ACME
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@ -1,12 +1,15 @@
|
|||||||
package main
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/satori/go.uuid"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/joohoi/acme-dns/pkg/acmedns"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetValidUsername(t *testing.T) {
|
func TestGetValidUsername(t *testing.T) {
|
||||||
v1, _ := uuid.FromString("a097455b-52cc-4569-90c8-7a4b97c6eba8")
|
v1, _ := uuid.Parse("a097455b-52cc-4569-90c8-7a4b97c6eba8")
|
||||||
for i, test := range []struct {
|
for i, test := range []struct {
|
||||||
uname string
|
uname string
|
||||||
output uuid.UUID
|
output uuid.UUID
|
||||||
@ -54,7 +57,9 @@ func TestGetValidSubdomain(t *testing.T) {
|
|||||||
output bool
|
output bool
|
||||||
}{
|
}{
|
||||||
{"a097455b-52cc-4569-90c8-7a4b97c6eba8", true},
|
{"a097455b-52cc-4569-90c8-7a4b97c6eba8", true},
|
||||||
{"a-97455b-52cc-4569-90c8-7a4b97c6eba8", false},
|
{"a-97455b-52cc-4569-90c8-7a4b97c6eba8", true},
|
||||||
|
{"foo.example.com", false},
|
||||||
|
{"foo-example-com", true},
|
||||||
{"", false},
|
{"", false},
|
||||||
{"&!#!25123!%!'%", false},
|
{"&!#!25123!%!'%", false},
|
||||||
} {
|
} {
|
||||||
@ -100,7 +105,7 @@ func TestCorrectPassword(t *testing.T) {
|
|||||||
false},
|
false},
|
||||||
{"", "", false},
|
{"", "", false},
|
||||||
} {
|
} {
|
||||||
ret := correctPassword(test.pw, test.hash)
|
ret := acmedns.CorrectPassword(test.pw, test.hash)
|
||||||
if ret != test.output {
|
if ret != test.output {
|
||||||
t.Errorf("Test %d: Expected return value %t, but got %t", i, test.output, ret)
|
t.Errorf("Test %d: Expected return value %t, but got %t", i, test.output, ret)
|
||||||
}
|
}
|
||||||
@ -109,12 +114,12 @@ func TestCorrectPassword(t *testing.T) {
|
|||||||
|
|
||||||
func TestGetValidCIDRMasks(t *testing.T) {
|
func TestGetValidCIDRMasks(t *testing.T) {
|
||||||
for i, test := range []struct {
|
for i, test := range []struct {
|
||||||
input cidrslice
|
input acmedns.Cidrslice
|
||||||
output cidrslice
|
output acmedns.Cidrslice
|
||||||
}{
|
}{
|
||||||
{cidrslice{"10.0.0.1/24"}, cidrslice{"10.0.0.1/24"}},
|
{acmedns.Cidrslice{"10.0.0.1/24"}, acmedns.Cidrslice{"10.0.0.1/24"}},
|
||||||
{cidrslice{"invalid", "127.0.0.1/32"}, cidrslice{"127.0.0.1/32"}},
|
{acmedns.Cidrslice{"invalid", "127.0.0.1/32"}, acmedns.Cidrslice{"127.0.0.1/32"}},
|
||||||
{cidrslice{"2002:c0a8::0/32", "8.8.8.8/32"}, cidrslice{"2002:c0a8::0/32", "8.8.8.8/32"}},
|
{acmedns.Cidrslice{"2002:c0a8::0/32", "8.8.8.8/32"}, acmedns.Cidrslice{"2002:c0a8::0/32", "8.8.8.8/32"}},
|
||||||
} {
|
} {
|
||||||
ret := test.input.ValidEntries()
|
ret := test.input.ValidEntries()
|
||||||
if len(ret) == len(test.output) {
|
if len(ret) == len(test.output) {
|
||||||
@ -1,21 +1,31 @@
|
|||||||
package main
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/glebarez/go-sqlite"
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
_ "github.com/mattn/go-sqlite3"
|
|
||||||
"github.com/satori/go.uuid"
|
"github.com/google/uuid"
|
||||||
log "github.com/sirupsen/logrus"
|
"go.uber.org/zap"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
|
"github.com/joohoi/acme-dns/pkg/acmedns"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type acmednsdb struct {
|
||||||
|
DB *sql.DB
|
||||||
|
Mutex sync.Mutex
|
||||||
|
Logger *zap.SugaredLogger
|
||||||
|
Config *acmedns.AcmeDnsConfig
|
||||||
|
}
|
||||||
|
|
||||||
// DBVersion shows the database version this code uses. This is used for update checks.
|
// DBVersion shows the database version this code uses. This is used for update checks.
|
||||||
var DBVersion = 1
|
var DBVersion = 1
|
||||||
|
|
||||||
@ -50,16 +60,17 @@ var txtTablePG = `
|
|||||||
|
|
||||||
// getSQLiteStmt replaces all PostgreSQL prepared statement placeholders (eg. $1, $2) with SQLite variant "?"
|
// getSQLiteStmt replaces all PostgreSQL prepared statement placeholders (eg. $1, $2) with SQLite variant "?"
|
||||||
func getSQLiteStmt(s string) string {
|
func getSQLiteStmt(s string) string {
|
||||||
re, _ := regexp.Compile("\\$[0-9]")
|
re, _ := regexp.Compile(`\$[0-9]`)
|
||||||
return re.ReplaceAllString(s, "?")
|
return re.ReplaceAllString(s, "?")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *acmedb) Init(engine string, connection string) error {
|
func Init(config *acmedns.AcmeDnsConfig, logger *zap.SugaredLogger) (acmedns.AcmednsDB, error) {
|
||||||
d.Lock()
|
var d = &acmednsdb{Config: config, Logger: logger}
|
||||||
defer d.Unlock()
|
d.Mutex.Lock()
|
||||||
db, err := sql.Open(engine, connection)
|
defer d.Mutex.Unlock()
|
||||||
|
db, err := sql.Open(config.Database.Engine, config.Database.Connection)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return d, err
|
||||||
}
|
}
|
||||||
d.DB = db
|
d.DB = db
|
||||||
// Check version first to try to catch old versions without version string
|
// Check version first to try to catch old versions without version string
|
||||||
@ -68,12 +79,12 @@ func (d *acmedb) Init(engine string, connection string) error {
|
|||||||
if versionString == "" {
|
if versionString == "" {
|
||||||
versionString = "0"
|
versionString = "0"
|
||||||
}
|
}
|
||||||
_, err = d.DB.Exec(acmeTable)
|
_, _ = d.DB.Exec(acmeTable)
|
||||||
_, err = d.DB.Exec(userTable)
|
_, _ = d.DB.Exec(userTable)
|
||||||
if Config.Database.Engine == "sqlite3" {
|
if config.Database.Engine == "sqlite" {
|
||||||
_, err = d.DB.Exec(txtTable)
|
_, _ = d.DB.Exec(txtTable)
|
||||||
} else {
|
} else {
|
||||||
_, err = d.DB.Exec(txtTablePG)
|
_, _ = d.DB.Exec(txtTablePG)
|
||||||
}
|
}
|
||||||
// If everything is fine, handle db upgrade tasks
|
// If everything is fine, handle db upgrade tasks
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -86,10 +97,10 @@ func (d *acmedb) Init(engine string, connection string) error {
|
|||||||
_, err = db.Exec(insversion)
|
_, err = db.Exec(insversion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return err
|
return d, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *acmedb) checkDBUpgrades(versionString string) error {
|
func (d *acmednsdb) checkDBUpgrades(versionString string) error {
|
||||||
var err error
|
var err error
|
||||||
version, err := strconv.Atoi(versionString)
|
version, err := strconv.Atoi(versionString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -102,19 +113,20 @@ func (d *acmedb) checkDBUpgrades(versionString string) error {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *acmedb) handleDBUpgrades(version int) error {
|
func (d *acmednsdb) handleDBUpgrades(version int) error {
|
||||||
if version == 0 {
|
if version == 0 {
|
||||||
return d.handleDBUpgradeTo1()
|
return d.handleDBUpgradeTo1()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *acmedb) handleDBUpgradeTo1() error {
|
func (d *acmednsdb) handleDBUpgradeTo1() error {
|
||||||
var err error
|
var err error
|
||||||
var subdomains []string
|
var subdomains []string
|
||||||
rows, err := d.DB.Query("SELECT Subdomain FROM records")
|
rows, err := d.DB.Query("SELECT Subdomain FROM records")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields(log.Fields{"error": err.Error()}).Error("Error in DB upgrade")
|
d.Logger.Errorw("Error in DB upgrade",
|
||||||
|
"error", err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
@ -122,24 +134,26 @@ func (d *acmedb) handleDBUpgradeTo1() error {
|
|||||||
var subdomain string
|
var subdomain string
|
||||||
err = rows.Scan(&subdomain)
|
err = rows.Scan(&subdomain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields(log.Fields{"error": err.Error()}).Error("Error in DB upgrade while reading values")
|
d.Logger.Errorw("Error in DB upgrade while reading values",
|
||||||
|
"error", err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
subdomains = append(subdomains, subdomain)
|
subdomains = append(subdomains, subdomain)
|
||||||
}
|
}
|
||||||
err = rows.Err()
|
err = rows.Err()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields(log.Fields{"error": err.Error()}).Error("Error in DB upgrade while inserting values")
|
d.Logger.Errorw("Error in DB upgrade while inserting values",
|
||||||
|
"error", err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
tx, err := d.DB.Begin()
|
tx, err := d.DB.Begin()
|
||||||
// Rollback if errored, commit if not
|
// Rollback if errored, commit if not
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
_ = tx.Rollback()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tx.Commit()
|
_ = tx.Commit()
|
||||||
}()
|
}()
|
||||||
_, _ = tx.Exec("DELETE FROM txt")
|
_, _ = tx.Exec("DELETE FROM txt")
|
||||||
for _, subdomain := range subdomains {
|
for _, subdomain := range subdomains {
|
||||||
@ -147,13 +161,14 @@ func (d *acmedb) handleDBUpgradeTo1() error {
|
|||||||
// Insert two rows for each subdomain to txt table
|
// Insert two rows for each subdomain to txt table
|
||||||
err = d.NewTXTValuesInTransaction(tx, subdomain)
|
err = d.NewTXTValuesInTransaction(tx, subdomain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields(log.Fields{"error": err.Error()}).Error("Error in DB upgrade while inserting values")
|
d.Logger.Errorw("Error in DB upgrade while inserting values",
|
||||||
|
"error", err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// SQLite doesn't support dropping columns
|
// SQLite doesn't support dropping columns
|
||||||
if Config.Database.Engine != "sqlite3" {
|
if d.Config.Database.Engine != "sqlite" {
|
||||||
_, _ = tx.Exec("ALTER TABLE records DROP COLUMN IF EXISTS Value")
|
_, _ = tx.Exec("ALTER TABLE records DROP COLUMN IF EXISTS Value")
|
||||||
_, _ = tx.Exec("ALTER TABLE records DROP COLUMN IF EXISTS LastActive")
|
_, _ = tx.Exec("ALTER TABLE records DROP COLUMN IF EXISTS LastActive")
|
||||||
}
|
}
|
||||||
@ -161,30 +176,30 @@ func (d *acmedb) handleDBUpgradeTo1() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create two rows for subdomain to the txt table
|
// NewTXTValuesInTransaction creates two rows for subdomain to the txt table
|
||||||
func (d *acmedb) NewTXTValuesInTransaction(tx *sql.Tx, subdomain string) error {
|
func (d *acmednsdb) NewTXTValuesInTransaction(tx *sql.Tx, subdomain string) error {
|
||||||
var err error
|
var err error
|
||||||
instr := fmt.Sprintf("INSERT INTO txt (Subdomain, LastUpdate) values('%s', 0)", subdomain)
|
instr := fmt.Sprintf("INSERT INTO txt (Subdomain, LastUpdate) values('%s', 0)", subdomain)
|
||||||
_, err = tx.Exec(instr)
|
_, _ = tx.Exec(instr)
|
||||||
_, err = tx.Exec(instr)
|
_, _ = tx.Exec(instr)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *acmedb) Register(afrom cidrslice) (ACMETxt, error) {
|
func (d *acmednsdb) Register(afrom acmedns.Cidrslice) (acmedns.ACMETxt, error) {
|
||||||
d.Lock()
|
d.Mutex.Lock()
|
||||||
defer d.Unlock()
|
defer d.Mutex.Unlock()
|
||||||
var err error
|
var err error
|
||||||
tx, err := d.DB.Begin()
|
tx, err := d.DB.Begin()
|
||||||
// Rollback if errored, commit if not
|
// Rollback if errored, commit if not
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
_ = tx.Rollback()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tx.Commit()
|
_ = tx.Commit()
|
||||||
}()
|
}()
|
||||||
a := newACMETxt()
|
a := acmedns.NewACMETxt()
|
||||||
a.AllowFrom = cidrslice(afrom.ValidEntries())
|
a.AllowFrom = acmedns.Cidrslice(afrom.ValidEntries())
|
||||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(a.Password), 10)
|
passwordHash, err := bcrypt.GenerateFromPassword([]byte(a.Password), 10)
|
||||||
regSQL := `
|
regSQL := `
|
||||||
INSERT INTO records(
|
INSERT INTO records(
|
||||||
@ -193,13 +208,14 @@ func (d *acmedb) Register(afrom cidrslice) (ACMETxt, error) {
|
|||||||
Subdomain,
|
Subdomain,
|
||||||
AllowFrom)
|
AllowFrom)
|
||||||
values($1, $2, $3, $4)`
|
values($1, $2, $3, $4)`
|
||||||
if Config.Database.Engine == "sqlite3" {
|
if d.Config.Database.Engine == "sqlite" {
|
||||||
regSQL = getSQLiteStmt(regSQL)
|
regSQL = getSQLiteStmt(regSQL)
|
||||||
}
|
}
|
||||||
sm, err := tx.Prepare(regSQL)
|
sm, err := tx.Prepare(regSQL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields(log.Fields{"error": err.Error()}).Error("Database error in prepare")
|
d.Logger.Errorw("Database error in prepare",
|
||||||
return a, errors.New("SQL error")
|
"error", err.Error())
|
||||||
|
return a, fmt.Errorf("failed to prepare registration statement: %w", err)
|
||||||
}
|
}
|
||||||
defer sm.Close()
|
defer sm.Close()
|
||||||
_, err = sm.Exec(a.Username.String(), passwordHash, a.Subdomain, a.AllowFrom.JSON())
|
_, err = sm.Exec(a.Username.String(), passwordHash, a.Subdomain, a.AllowFrom.JSON())
|
||||||
@ -209,53 +225,53 @@ func (d *acmedb) Register(afrom cidrslice) (ACMETxt, error) {
|
|||||||
return a, err
|
return a, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *acmedb) GetByUsername(u uuid.UUID) (ACMETxt, error) {
|
func (d *acmednsdb) GetByUsername(u uuid.UUID) (acmedns.ACMETxt, error) {
|
||||||
d.Lock()
|
d.Mutex.Lock()
|
||||||
defer d.Unlock()
|
defer d.Mutex.Unlock()
|
||||||
var results []ACMETxt
|
var results []acmedns.ACMETxt
|
||||||
getSQL := `
|
getSQL := `
|
||||||
SELECT Username, Password, Subdomain, AllowFrom
|
SELECT Username, Password, Subdomain, AllowFrom
|
||||||
FROM records
|
FROM records
|
||||||
WHERE Username=$1 LIMIT 1
|
WHERE Username=$1 LIMIT 1
|
||||||
`
|
`
|
||||||
if Config.Database.Engine == "sqlite3" {
|
if d.Config.Database.Engine == "sqlite" {
|
||||||
getSQL = getSQLiteStmt(getSQL)
|
getSQL = getSQLiteStmt(getSQL)
|
||||||
}
|
}
|
||||||
|
|
||||||
sm, err := d.DB.Prepare(getSQL)
|
sm, err := d.DB.Prepare(getSQL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ACMETxt{}, err
|
return acmedns.ACMETxt{}, err
|
||||||
}
|
}
|
||||||
defer sm.Close()
|
defer sm.Close()
|
||||||
rows, err := sm.Query(u.String())
|
rows, err := sm.Query(u.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ACMETxt{}, err
|
return acmedns.ACMETxt{}, fmt.Errorf("failed to query user: %w", err)
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
// It will only be one row though
|
// It will only be one row though
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
txt, err := getModelFromRow(rows)
|
txt, err := d.getModelFromRow(rows)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ACMETxt{}, err
|
return acmedns.ACMETxt{}, err
|
||||||
}
|
}
|
||||||
results = append(results, txt)
|
results = append(results, txt)
|
||||||
}
|
}
|
||||||
if len(results) > 0 {
|
if len(results) > 0 {
|
||||||
return results[0], nil
|
return results[0], nil
|
||||||
}
|
}
|
||||||
return ACMETxt{}, errors.New("no user")
|
return acmedns.ACMETxt{}, fmt.Errorf("user not found: %s", u.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *acmedb) GetTXTForDomain(domain string) ([]string, error) {
|
func (d *acmednsdb) GetTXTForDomain(domain string) ([]string, error) {
|
||||||
d.Lock()
|
d.Mutex.Lock()
|
||||||
defer d.Unlock()
|
defer d.Mutex.Unlock()
|
||||||
domain = sanitizeString(domain)
|
domain = acmedns.SanitizeString(domain)
|
||||||
var txts []string
|
var txts []string
|
||||||
getSQL := `
|
getSQL := `
|
||||||
SELECT Value FROM txt WHERE Subdomain=$1 LIMIT 2
|
SELECT Value FROM txt WHERE Subdomain=$1 LIMIT 2
|
||||||
`
|
`
|
||||||
if Config.Database.Engine == "sqlite3" {
|
if d.Config.Database.Engine == "sqlite" {
|
||||||
getSQL = getSQLiteStmt(getSQL)
|
getSQL = getSQLiteStmt(getSQL)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,9 +297,9 @@ func (d *acmedb) GetTXTForDomain(domain string) ([]string, error) {
|
|||||||
return txts, nil
|
return txts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *acmedb) Update(a ACMETxt) error {
|
func (d *acmednsdb) Update(a acmedns.ACMETxtPost) error {
|
||||||
d.Lock()
|
d.Mutex.Lock()
|
||||||
defer d.Unlock()
|
defer d.Mutex.Unlock()
|
||||||
var err error
|
var err error
|
||||||
// Data in a is already sanitized
|
// Data in a is already sanitized
|
||||||
timenow := time.Now().Unix()
|
timenow := time.Now().Unix()
|
||||||
@ -293,7 +309,7 @@ func (d *acmedb) Update(a ACMETxt) error {
|
|||||||
WHERE rowid=(
|
WHERE rowid=(
|
||||||
SELECT rowid FROM txt WHERE Subdomain=$3 ORDER BY LastUpdate LIMIT 1)
|
SELECT rowid FROM txt WHERE Subdomain=$3 ORDER BY LastUpdate LIMIT 1)
|
||||||
`
|
`
|
||||||
if Config.Database.Engine == "sqlite3" {
|
if d.Config.Database.Engine == "sqlite" {
|
||||||
updSQL = getSQLiteStmt(updSQL)
|
updSQL = getSQLiteStmt(updSQL)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -309,8 +325,8 @@ func (d *acmedb) Update(a ACMETxt) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getModelFromRow(r *sql.Rows) (ACMETxt, error) {
|
func (d *acmednsdb) getModelFromRow(r *sql.Rows) (acmedns.ACMETxt, error) {
|
||||||
txt := ACMETxt{}
|
txt := acmedns.ACMETxt{}
|
||||||
afrom := ""
|
afrom := ""
|
||||||
err := r.Scan(
|
err := r.Scan(
|
||||||
&txt.Username,
|
&txt.Username,
|
||||||
@ -318,26 +334,28 @@ func getModelFromRow(r *sql.Rows) (ACMETxt, error) {
|
|||||||
&txt.Subdomain,
|
&txt.Subdomain,
|
||||||
&afrom)
|
&afrom)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields(log.Fields{"error": err.Error()}).Error("Row scan error")
|
d.Logger.Errorw("Row scan error",
|
||||||
|
"error", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
cslice := cidrslice{}
|
cslice := acmedns.Cidrslice{}
|
||||||
err = json.Unmarshal([]byte(afrom), &cslice)
|
err = json.Unmarshal([]byte(afrom), &cslice)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields(log.Fields{"error": err.Error()}).Error("JSON unmarshall error")
|
d.Logger.Errorw("JSON unmarshall error",
|
||||||
|
"error", err.Error())
|
||||||
}
|
}
|
||||||
txt.AllowFrom = cslice
|
txt.AllowFrom = cslice
|
||||||
return txt, err
|
return txt, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *acmedb) Close() {
|
func (d *acmednsdb) Close() {
|
||||||
d.DB.Close()
|
d.DB.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *acmedb) GetBackend() *sql.DB {
|
func (d *acmednsdb) GetBackend() *sql.DB {
|
||||||
return d.DB
|
return d.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *acmedb) SetBackend(backend *sql.DB) {
|
func (d *acmednsdb) SetBackend(backend *sql.DB) {
|
||||||
d.DB = backend
|
d.DB = backend
|
||||||
}
|
}
|
||||||
@ -1,11 +1,15 @@
|
|||||||
package main
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"database/sql/driver"
|
"database/sql/driver"
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/erikstmartin/go-testdb"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/erikstmartin/go-testdb"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/joohoi/acme-dns/pkg/acmedns"
|
||||||
)
|
)
|
||||||
|
|
||||||
type testResult struct {
|
type testResult struct {
|
||||||
@ -21,42 +25,38 @@ func (r testResult) RowsAffected() (int64, error) {
|
|||||||
return r.affectedRows, nil
|
return r.affectedRows, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDBInit(t *testing.T) {
|
func fakeConfigAndLogger() (acmedns.AcmeDnsConfig, *zap.SugaredLogger) {
|
||||||
fakeDB := new(acmedb)
|
c := acmedns.AcmeDnsConfig{}
|
||||||
err := fakeDB.Init("notarealegine", "connectionstring")
|
c.Database.Engine = "sqlite"
|
||||||
if err == nil {
|
c.Database.Connection = ":memory:"
|
||||||
t.Errorf("Was expecting error, didn't get one.")
|
l := zap.NewNop().Sugar()
|
||||||
}
|
return c, l
|
||||||
|
}
|
||||||
|
|
||||||
testdb.SetExecWithArgsFunc(func(query string, args []driver.Value) (result driver.Result, err error) {
|
func fakeDB() acmedns.AcmednsDB {
|
||||||
return testResult{1, 0}, errors.New("Prepared query error")
|
conf, logger := fakeConfigAndLogger()
|
||||||
})
|
db, _ := Init(&conf, logger)
|
||||||
defer testdb.Reset()
|
return db
|
||||||
|
|
||||||
errorDB := new(acmedb)
|
|
||||||
err = errorDB.Init("testdb", "")
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("Was expecting DB initiation error but got none")
|
|
||||||
}
|
|
||||||
errorDB.Close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRegisterNoCIDR(t *testing.T) {
|
func TestRegisterNoCIDR(t *testing.T) {
|
||||||
// Register tests
|
// Register tests
|
||||||
_, err := DB.Register(cidrslice{})
|
DB := fakeDB()
|
||||||
|
_, err := DB.Register(acmedns.Cidrslice{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Registration failed, got error [%v]", err)
|
t.Errorf("Registration failed, got error [%v]", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRegisterMany(t *testing.T) {
|
func TestRegisterMany(t *testing.T) {
|
||||||
|
DB := fakeDB()
|
||||||
for i, test := range []struct {
|
for i, test := range []struct {
|
||||||
input cidrslice
|
input acmedns.Cidrslice
|
||||||
output cidrslice
|
output acmedns.Cidrslice
|
||||||
}{
|
}{
|
||||||
{cidrslice{"127.0.0.1/8", "8.8.8.8/32", "1.0.0.1/1"}, cidrslice{"127.0.0.1/8", "8.8.8.8/32", "1.0.0.1/1"}},
|
{acmedns.Cidrslice{"127.0.0.1/8", "8.8.8.8/32", "1.0.0.1/1"}, acmedns.Cidrslice{"127.0.0.1/8", "8.8.8.8/32", "1.0.0.1/1"}},
|
||||||
{cidrslice{"1.1.1./32", "1922.168.42.42/8", "1.1.1.1/33", "1.2.3.4/"}, cidrslice{}},
|
{acmedns.Cidrslice{"1.1.1./32", "1922.168.42.42/8", "1.1.1.1/33", "1.2.3.4/"}, acmedns.Cidrslice{}},
|
||||||
{cidrslice{"7.6.5.4/32", "invalid", "1.0.0.1/2"}, cidrslice{"7.6.5.4/32", "1.0.0.1/2"}},
|
{acmedns.Cidrslice{"7.6.5.4/32", "invalid", "1.0.0.1/2"}, acmedns.Cidrslice{"7.6.5.4/32", "1.0.0.1/2"}},
|
||||||
} {
|
} {
|
||||||
user, err := DB.Register(test.input)
|
user, err := DB.Register(test.input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -67,18 +67,19 @@ func TestRegisterMany(t *testing.T) {
|
|||||||
t.Errorf("Test %d: Got error when fetching username: [%v]", i, err)
|
t.Errorf("Test %d: Got error when fetching username: [%v]", i, err)
|
||||||
}
|
}
|
||||||
if len(user.AllowFrom) != len(test.output) {
|
if len(user.AllowFrom) != len(test.output) {
|
||||||
t.Errorf("Test %d: Expected to recieve struct with [%d] entries in AllowFrom, but got [%d] records", i, len(test.output), len(user.AllowFrom))
|
t.Errorf("Test %d: Expected to receive struct with [%d] entries in AllowFrom, but got [%d] records", i, len(test.output), len(user.AllowFrom))
|
||||||
}
|
}
|
||||||
if len(res.AllowFrom) != len(test.output) {
|
if len(res.AllowFrom) != len(test.output) {
|
||||||
t.Errorf("Test %d: Expected to recieve struct with [%d] entries in AllowFrom, but got [%d] records", i, len(test.output), len(res.AllowFrom))
|
t.Errorf("Test %d: Expected to receive struct with [%d] entries in AllowFrom, but got [%d] records", i, len(test.output), len(res.AllowFrom))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetByUsername(t *testing.T) {
|
func TestGetByUsername(t *testing.T) {
|
||||||
|
DB := fakeDB()
|
||||||
// Create reg to refer to
|
// Create reg to refer to
|
||||||
reg, err := DB.Register(cidrslice{})
|
reg, err := DB.Register(acmedns.Cidrslice{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Registration failed, got error [%v]", err)
|
t.Errorf("Registration failed, got error [%v]", err)
|
||||||
}
|
}
|
||||||
@ -97,13 +98,14 @@ func TestGetByUsername(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// regUser password already is a bcrypt hash
|
// regUser password already is a bcrypt hash
|
||||||
if !correctPassword(reg.Password, regUser.Password) {
|
if !acmedns.CorrectPassword(reg.Password, regUser.Password) {
|
||||||
t.Errorf("The password [%s] does not match the hash [%s]", reg.Password, regUser.Password)
|
t.Errorf("The password [%s] does not match the hash [%s]", reg.Password, regUser.Password)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPrepareErrors(t *testing.T) {
|
func TestPrepareErrors(t *testing.T) {
|
||||||
reg, _ := DB.Register(cidrslice{})
|
DB := fakeDB()
|
||||||
|
reg, _ := DB.Register(acmedns.Cidrslice{})
|
||||||
tdb, err := sql.Open("testdb", "")
|
tdb, err := sql.Open("testdb", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Got error: %v", err)
|
t.Errorf("Got error: %v", err)
|
||||||
@ -125,7 +127,8 @@ func TestPrepareErrors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestQueryExecErrors(t *testing.T) {
|
func TestQueryExecErrors(t *testing.T) {
|
||||||
reg, _ := DB.Register(cidrslice{})
|
DB := fakeDB()
|
||||||
|
reg, _ := DB.Register(acmedns.Cidrslice{})
|
||||||
testdb.SetExecWithArgsFunc(func(query string, args []driver.Value) (result driver.Result, err error) {
|
testdb.SetExecWithArgsFunc(func(query string, args []driver.Value) (result driver.Result, err error) {
|
||||||
return testResult{1, 0}, errors.New("Prepared query error")
|
return testResult{1, 0}, errors.New("Prepared query error")
|
||||||
})
|
})
|
||||||
@ -156,12 +159,12 @@ func TestQueryExecErrors(t *testing.T) {
|
|||||||
t.Errorf("Expected error from exec in GetByDomain, but got none")
|
t.Errorf("Expected error from exec in GetByDomain, but got none")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = DB.Register(cidrslice{})
|
_, err = DB.Register(acmedns.Cidrslice{})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("Expected error from exec in Register, but got none")
|
t.Errorf("Expected error from exec in Register, but got none")
|
||||||
}
|
}
|
||||||
reg.Value = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
reg.Value = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
err = DB.Update(reg)
|
err = DB.Update(reg.ACMETxtPost)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("Expected error from exec in Update, but got none")
|
t.Errorf("Expected error from exec in Update, but got none")
|
||||||
}
|
}
|
||||||
@ -169,7 +172,8 @@ func TestQueryExecErrors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestQueryScanErrors(t *testing.T) {
|
func TestQueryScanErrors(t *testing.T) {
|
||||||
reg, _ := DB.Register(cidrslice{})
|
DB := fakeDB()
|
||||||
|
reg, _ := DB.Register(acmedns.Cidrslice{})
|
||||||
|
|
||||||
testdb.SetExecWithArgsFunc(func(query string, args []driver.Value) (result driver.Result, err error) {
|
testdb.SetExecWithArgsFunc(func(query string, args []driver.Value) (result driver.Result, err error) {
|
||||||
return testResult{1, 0}, errors.New("Prepared query error")
|
return testResult{1, 0}, errors.New("Prepared query error")
|
||||||
@ -198,7 +202,8 @@ func TestQueryScanErrors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBadDBValues(t *testing.T) {
|
func TestBadDBValues(t *testing.T) {
|
||||||
reg, _ := DB.Register(cidrslice{})
|
DB := fakeDB()
|
||||||
|
reg, _ := DB.Register(acmedns.Cidrslice{})
|
||||||
|
|
||||||
testdb.SetQueryWithArgsFunc(func(query string, args []driver.Value) (result driver.Rows, err error) {
|
testdb.SetQueryWithArgsFunc(func(query string, args []driver.Value) (result driver.Rows, err error) {
|
||||||
columns := []string{"Username", "Password", "Subdomain", "Value", "LastActive"}
|
columns := []string{"Username", "Password", "Subdomain", "Value", "LastActive"}
|
||||||
@ -228,8 +233,9 @@ func TestBadDBValues(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGetTXTForDomain(t *testing.T) {
|
func TestGetTXTForDomain(t *testing.T) {
|
||||||
|
DB := fakeDB()
|
||||||
// Create reg to refer to
|
// Create reg to refer to
|
||||||
reg, err := DB.Register(cidrslice{})
|
reg, err := DB.Register(acmedns.Cidrslice{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Registration failed, got error [%v]", err)
|
t.Errorf("Registration failed, got error [%v]", err)
|
||||||
}
|
}
|
||||||
@ -238,10 +244,10 @@ func TestGetTXTForDomain(t *testing.T) {
|
|||||||
txtval2 := "___validation_token_received_YEAH_the_ca___"
|
txtval2 := "___validation_token_received_YEAH_the_ca___"
|
||||||
|
|
||||||
reg.Value = txtval1
|
reg.Value = txtval1
|
||||||
_ = DB.Update(reg)
|
_ = DB.Update(reg.ACMETxtPost)
|
||||||
|
|
||||||
reg.Value = txtval2
|
reg.Value = txtval2
|
||||||
_ = DB.Update(reg)
|
_ = DB.Update(reg.ACMETxtPost)
|
||||||
|
|
||||||
regDomainSlice, err := DB.GetTXTForDomain(reg.Subdomain)
|
regDomainSlice, err := DB.GetTXTForDomain(reg.Subdomain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -276,8 +282,9 @@ func TestGetTXTForDomain(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUpdate(t *testing.T) {
|
func TestUpdate(t *testing.T) {
|
||||||
|
DB := fakeDB()
|
||||||
// Create reg to refer to
|
// Create reg to refer to
|
||||||
reg, err := DB.Register(cidrslice{})
|
reg, err := DB.Register(acmedns.Cidrslice{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Registration failed, got error [%v]", err)
|
t.Errorf("Registration failed, got error [%v]", err)
|
||||||
}
|
}
|
||||||
@ -294,7 +301,7 @@ func TestUpdate(t *testing.T) {
|
|||||||
regUser.Password = "nevergonnagiveyouup"
|
regUser.Password = "nevergonnagiveyouup"
|
||||||
regUser.Value = validTXT
|
regUser.Value = validTXT
|
||||||
|
|
||||||
err = DB.Update(regUser)
|
err = DB.Update(regUser.ACMETxtPost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("DB Update failed, got error: [%v]", err)
|
t.Errorf("DB Update failed, got error: [%v]", err)
|
||||||
}
|
}
|
||||||
396
pkg/nameserver/dns_test.go
Normal file
396
pkg/nameserver/dns_test.go
Normal file
@ -0,0 +1,396 @@
|
|||||||
|
package nameserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"database/sql/driver"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/erikstmartin/go-testdb"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zaptest/observer"
|
||||||
|
|
||||||
|
"github.com/joohoi/acme-dns/pkg/acmedns"
|
||||||
|
"github.com/joohoi/acme-dns/pkg/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
type resolver struct {
|
||||||
|
server string
|
||||||
|
}
|
||||||
|
|
||||||
|
var records = []string{
|
||||||
|
"auth.example.org. A 192.168.1.100",
|
||||||
|
"ns1.auth.example.org. A 192.168.1.101",
|
||||||
|
"cn.example.org CNAME something.example.org.",
|
||||||
|
"!''b', unparseable ",
|
||||||
|
"ns2.auth.example.org. A 192.168.1.102",
|
||||||
|
}
|
||||||
|
|
||||||
|
func loggerHasEntryWithMessage(message string, logObserver *observer.ObservedLogs) bool {
|
||||||
|
return len(logObserver.FilterMessage(message).All()) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func fakeConfigAndLogger() (acmedns.AcmeDnsConfig, *zap.SugaredLogger, *observer.ObservedLogs) {
|
||||||
|
c := acmedns.AcmeDnsConfig{}
|
||||||
|
c.Database.Engine = "sqlite"
|
||||||
|
c.Database.Connection = ":memory:"
|
||||||
|
obsCore, logObserver := observer.New(zap.DebugLevel)
|
||||||
|
obsLogger := zap.New(obsCore).Sugar()
|
||||||
|
return c, obsLogger, logObserver
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupDNS() (acmedns.AcmednsNS, acmedns.AcmednsDB, *observer.ObservedLogs) {
|
||||||
|
config, logger, logObserver := fakeConfigAndLogger()
|
||||||
|
config.General.Domain = "auth.example.org"
|
||||||
|
config.General.Listen = "127.0.0.1:15353"
|
||||||
|
config.General.Proto = "udp"
|
||||||
|
config.General.Nsname = "ns1.auth.example.org"
|
||||||
|
config.General.Nsadmin = "admin.example.org"
|
||||||
|
config.General.StaticRecords = records
|
||||||
|
config.General.Debug = false
|
||||||
|
db, _ := database.Init(&config, logger)
|
||||||
|
server := Nameserver{Config: &config, DB: db, Logger: logger, personalAuthKey: ""}
|
||||||
|
server.Domains = make(map[string]Records)
|
||||||
|
server.Server = &dns.Server{Addr: config.General.Listen, Net: config.General.Proto}
|
||||||
|
server.ParseRecords()
|
||||||
|
server.OwnDomain = "auth.example.org."
|
||||||
|
return &server, db, logObserver
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resolver) lookup(host string, qtype uint16) (*dns.Msg, error) {
|
||||||
|
msg := new(dns.Msg)
|
||||||
|
msg.Id = dns.Id()
|
||||||
|
msg.Question = make([]dns.Question, 1)
|
||||||
|
msg.Question[0] = dns.Question{Name: dns.Fqdn(host), Qtype: qtype, Qclass: dns.ClassINET}
|
||||||
|
in, err := dns.Exchange(msg, r.server)
|
||||||
|
if err != nil {
|
||||||
|
return in, fmt.Errorf("Error querying the server [%v]", err)
|
||||||
|
}
|
||||||
|
if in != nil && in.Rcode != dns.RcodeSuccess {
|
||||||
|
return in, fmt.Errorf("Received error from the server [%s]", dns.RcodeToString[in.Rcode])
|
||||||
|
}
|
||||||
|
return in, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQuestionDBError(t *testing.T) {
|
||||||
|
config, logger, _ := fakeConfigAndLogger()
|
||||||
|
config.General.Listen = "127.0.0.1:15353"
|
||||||
|
config.General.Proto = "udp"
|
||||||
|
config.General.Domain = "auth.example.org"
|
||||||
|
config.General.Nsname = "ns1.auth.example.org"
|
||||||
|
config.General.Nsadmin = "admin.example.org"
|
||||||
|
config.General.StaticRecords = records
|
||||||
|
config.General.Debug = false
|
||||||
|
db, _ := database.Init(&config, logger)
|
||||||
|
server := Nameserver{Config: &config, DB: db, Logger: logger, personalAuthKey: ""}
|
||||||
|
server.Domains = make(map[string]Records)
|
||||||
|
server.ParseRecords()
|
||||||
|
testdb.SetQueryWithArgsFunc(func(query string, args []driver.Value) (result driver.Rows, err error) {
|
||||||
|
columns := []string{"Username", "Password", "Subdomain", "Value", "LastActive"}
|
||||||
|
return testdb.RowsFromSlice(columns, [][]driver.Value{}), errors.New("Prepared query error")
|
||||||
|
})
|
||||||
|
|
||||||
|
defer testdb.Reset()
|
||||||
|
|
||||||
|
tdb, err := sql.Open("testdb", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Got error: %v", err)
|
||||||
|
}
|
||||||
|
oldDb := db.GetBackend()
|
||||||
|
|
||||||
|
db.SetBackend(tdb)
|
||||||
|
defer db.SetBackend(oldDb)
|
||||||
|
|
||||||
|
q := dns.Question{Name: dns.Fqdn("whatever.tld"), Qtype: dns.TypeTXT, Qclass: dns.ClassINET}
|
||||||
|
_, err = server.answerTXT(q)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected error but got none")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParse(t *testing.T) {
|
||||||
|
config, logger, logObserver := fakeConfigAndLogger()
|
||||||
|
config.General.Listen = "127.0.0.1:15353"
|
||||||
|
config.General.Proto = "udp"
|
||||||
|
config.General.Domain = ")"
|
||||||
|
config.General.Nsname = "ns1.auth.example.org"
|
||||||
|
config.General.Nsadmin = "admin.example.org"
|
||||||
|
config.General.StaticRecords = records
|
||||||
|
config.General.Debug = false
|
||||||
|
config.General.StaticRecords = []string{}
|
||||||
|
db, _ := database.Init(&config, logger)
|
||||||
|
server := Nameserver{Config: &config, DB: db, Logger: logger, personalAuthKey: ""}
|
||||||
|
server.Domains = make(map[string]Records)
|
||||||
|
server.ParseRecords()
|
||||||
|
if !loggerHasEntryWithMessage("Error while adding SOA record", logObserver) {
|
||||||
|
t.Errorf("Expected SOA parsing to return error, but did not find one")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveA(t *testing.T) {
|
||||||
|
server, _, _ := setupDNS()
|
||||||
|
errChan := make(chan error, 1)
|
||||||
|
waitLock := sync.Mutex{}
|
||||||
|
waitLock.Lock()
|
||||||
|
server.SetNotifyStartedFunc(waitLock.Unlock)
|
||||||
|
go server.Start(errChan)
|
||||||
|
waitLock.Lock()
|
||||||
|
resolv := resolver{server: "127.0.0.1:15353"}
|
||||||
|
answer, err := resolv.lookup("auth.example.org", dns.TypeA)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(answer.Answer) == 0 {
|
||||||
|
t.Error("No answer for DNS query")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = resolv.lookup("nonexistent.domain.tld", dns.TypeA)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Was expecting error because of NXDOMAIN but got none")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEDNS(t *testing.T) {
|
||||||
|
resolv := resolver{server: "127.0.0.1:15353"}
|
||||||
|
answer, _ := resolv.lookup("auth.example.org", dns.TypeOPT)
|
||||||
|
if answer.Rcode != dns.RcodeSuccess {
|
||||||
|
t.Errorf("Was expecing NOERROR rcode for OPT query, but got [%s] instead.", dns.RcodeToString[answer.Rcode])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEDNSA(t *testing.T) {
|
||||||
|
msg := new(dns.Msg)
|
||||||
|
msg.Id = dns.Id()
|
||||||
|
msg.Question = make([]dns.Question, 1)
|
||||||
|
msg.Question[0] = dns.Question{Name: dns.Fqdn("auth.example.org"), Qtype: dns.TypeA, Qclass: dns.ClassINET}
|
||||||
|
// Set EDNS0 with DO=1
|
||||||
|
msg.SetEdns0(512, true)
|
||||||
|
in, err := dns.Exchange(msg, "127.0.0.1:15353")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error querying the server [%v]", err)
|
||||||
|
}
|
||||||
|
if in != nil && in.Rcode != dns.RcodeSuccess {
|
||||||
|
t.Errorf("Received error from the server [%s]", dns.RcodeToString[in.Rcode])
|
||||||
|
}
|
||||||
|
opt := in.IsEdns0()
|
||||||
|
if opt == nil {
|
||||||
|
t.Errorf("Should have got OPT back")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEDNSBADVERS(t *testing.T) {
|
||||||
|
msg := new(dns.Msg)
|
||||||
|
msg.Id = dns.Id()
|
||||||
|
msg.Question = make([]dns.Question, 1)
|
||||||
|
msg.Question[0] = dns.Question{Name: dns.Fqdn("auth.example.org"), Qtype: dns.TypeA, Qclass: dns.ClassINET}
|
||||||
|
// Set EDNS0 with version 1
|
||||||
|
o := new(dns.OPT)
|
||||||
|
o.SetVersion(1)
|
||||||
|
o.Hdr.Name = "."
|
||||||
|
o.Hdr.Rrtype = dns.TypeOPT
|
||||||
|
msg.Extra = append(msg.Extra, o)
|
||||||
|
in, err := dns.Exchange(msg, "127.0.0.1:15353")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error querying the server [%v]", err)
|
||||||
|
}
|
||||||
|
if in != nil && in.Rcode != dns.RcodeBadVers {
|
||||||
|
t.Errorf("Received unexpected rcode from the server [%s]", dns.RcodeToString[in.Rcode])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveCNAME(t *testing.T) {
|
||||||
|
resolv := resolver{server: "127.0.0.1:15353"}
|
||||||
|
expected := "cn.example.org. 3600 IN CNAME something.example.org."
|
||||||
|
answer, err := resolv.lookup("cn.example.org", dns.TypeCNAME)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Got unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
if len(answer.Answer) != 1 {
|
||||||
|
t.Errorf("Expected exactly 1 RR in answer, but got %d instead.", len(answer.Answer))
|
||||||
|
}
|
||||||
|
if answer.Answer[0].Header().Rrtype != dns.TypeCNAME {
|
||||||
|
t.Errorf("Expected a CNAME answer, but got [%s] instead.", dns.TypeToString[answer.Answer[0].Header().Rrtype])
|
||||||
|
}
|
||||||
|
if answer.Answer[0].String() != expected {
|
||||||
|
t.Errorf("Expected CNAME answer [%s] but got [%s] instead.", expected, answer.Answer[0].String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthoritative(t *testing.T) {
|
||||||
|
resolv := resolver{server: "127.0.0.1:15353"}
|
||||||
|
answer, _ := resolv.lookup("nonexistent.auth.example.org", dns.TypeA)
|
||||||
|
if answer.Rcode != dns.RcodeNameError {
|
||||||
|
t.Errorf("Was expecing NXDOMAIN rcode, but got [%s] instead.", dns.RcodeToString[answer.Rcode])
|
||||||
|
}
|
||||||
|
if len(answer.Ns) != 1 {
|
||||||
|
t.Errorf("Was expecting exactly one answer (SOA) for invalid subdomain, but got %d", len(answer.Ns))
|
||||||
|
}
|
||||||
|
if answer.Ns[0].Header().Rrtype != dns.TypeSOA {
|
||||||
|
t.Errorf("Was expecting SOA record as answer for NXDOMAIN but got [%s]", dns.TypeToString[answer.Ns[0].Header().Rrtype])
|
||||||
|
}
|
||||||
|
if !answer.Authoritative {
|
||||||
|
t.Errorf("Was expecting authoritative bit to be set")
|
||||||
|
}
|
||||||
|
nanswer, _ := resolv.lookup("nonexsitent.nonauth.tld", dns.TypeA)
|
||||||
|
if len(nanswer.Answer) > 0 {
|
||||||
|
t.Errorf("Didn't expect answers for non authotitative domain query")
|
||||||
|
}
|
||||||
|
if nanswer.Authoritative {
|
||||||
|
t.Errorf("Authoritative bit should not be set for non-authoritative domain.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveTXT(t *testing.T) {
|
||||||
|
iServer, db, _ := setupDNS()
|
||||||
|
server := iServer.(*Nameserver)
|
||||||
|
var validTXT string
|
||||||
|
// acme-dns validation in pkg/api/util.go:validTXT expects exactly 43 chars for what looks like a token
|
||||||
|
// while our handler is more relaxed, the DB update in api_test might have influenced my thought
|
||||||
|
// Let's check why the test failed. Ah, "Received error from the server [REFUSED]"? No, "NXDOMAIN"?
|
||||||
|
// Wait, the failure was: "Test 0: Expected answer but got: Received error from the server [SERVFAIL]"
|
||||||
|
// Or was it? The log was truncated.
|
||||||
|
// Actually, the registration atxt.Value is NOT used for Update, it uses ACMETxtPost.
|
||||||
|
// ACMETxtPost.Value needs to be valid.
|
||||||
|
|
||||||
|
atxt, err := db.Register(acmedns.Cidrslice{})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Could not initiate db record: [%v]", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
update := acmedns.ACMETxtPost{
|
||||||
|
Subdomain: atxt.Subdomain,
|
||||||
|
Value: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", // 43 chars
|
||||||
|
}
|
||||||
|
validTXT = update.Value
|
||||||
|
|
||||||
|
err = db.Update(update)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Could not update db record: [%v]", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range []struct {
|
||||||
|
subDomain string
|
||||||
|
expTXT string
|
||||||
|
getAnswer bool
|
||||||
|
validAnswer bool
|
||||||
|
}{
|
||||||
|
{atxt.Subdomain, validTXT, true, true},
|
||||||
|
{atxt.Subdomain, "invalid", true, false},
|
||||||
|
{"a097455b-52cc-4569-90c8-7a4b97c6eba8", validTXT, false, false},
|
||||||
|
} {
|
||||||
|
q := dns.Question{Name: dns.Fqdn(test.subDomain + ".auth.example.org"), Qtype: dns.TypeTXT, Qclass: dns.ClassINET}
|
||||||
|
ansRRs, rcode, _, err := server.answer(q)
|
||||||
|
if err != nil {
|
||||||
|
if test.getAnswer {
|
||||||
|
t.Fatalf("Test %d: Expected answer but got: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ansRRs) > 0 {
|
||||||
|
if !test.getAnswer && rcode == dns.RcodeNameError {
|
||||||
|
t.Errorf("Test %d: Expected no answer, but got: [%v]", i, ansRRs)
|
||||||
|
}
|
||||||
|
if test.getAnswer {
|
||||||
|
err = hasExpectedTXTAnswer(ansRRs, test.expTXT)
|
||||||
|
if err != nil {
|
||||||
|
if test.validAnswer {
|
||||||
|
t.Errorf("Test %d: %v", i, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !test.validAnswer {
|
||||||
|
t.Errorf("Test %d: Answer was not expected to be valid, answer [%q], compared to [%s]", i, ansRRs, test.expTXT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if test.getAnswer {
|
||||||
|
t.Errorf("Test %d: Expected answer, but didn't get one", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasExpectedTXTAnswer(answer []dns.RR, cmpTXT string) error {
|
||||||
|
for _, record := range answer {
|
||||||
|
// We expect only one answer, so no need to loop through the answer slice
|
||||||
|
if rec, ok := record.(*dns.TXT); ok {
|
||||||
|
for _, txtValue := range rec.Txt {
|
||||||
|
if txtValue == cmpTXT {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errmsg := fmt.Sprintf("Got answer of unexpected type [%q]", answer[0])
|
||||||
|
return errors.New(errmsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.New("Expected answer not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnswerTXTError(t *testing.T) {
|
||||||
|
config, logger, _ := fakeConfigAndLogger()
|
||||||
|
db, _ := database.Init(&config, logger)
|
||||||
|
server := Nameserver{Config: &config, DB: db, Logger: logger}
|
||||||
|
|
||||||
|
testdb.SetQueryWithArgsFunc(func(query string, args []driver.Value) (result driver.Rows, err error) {
|
||||||
|
return testdb.RowsFromSlice([]string{}, [][]driver.Value{}), errors.New("DB error")
|
||||||
|
})
|
||||||
|
defer testdb.Reset()
|
||||||
|
|
||||||
|
tdb, _ := sql.Open("testdb", "")
|
||||||
|
oldDb := db.GetBackend()
|
||||||
|
db.SetBackend(tdb)
|
||||||
|
defer db.SetBackend(oldDb)
|
||||||
|
|
||||||
|
q := dns.Question{Name: "whatever.auth.example.org.", Qtype: dns.TypeTXT}
|
||||||
|
_, err := server.answerTXT(q)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected error from answerTXT when DB fails, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnswerNameError(t *testing.T) {
|
||||||
|
iServer, _, _ := setupDNS()
|
||||||
|
server := iServer.(*Nameserver)
|
||||||
|
q := dns.Question{Name: "notauth.com.", Qtype: dns.TypeA}
|
||||||
|
_, rcode, auth, _ := server.answer(q)
|
||||||
|
if rcode != dns.RcodeNameError {
|
||||||
|
t.Errorf("Expected NXDOMAIN for non-authoritative domain, got %s", dns.RcodeToString[rcode])
|
||||||
|
}
|
||||||
|
if auth {
|
||||||
|
t.Errorf("Expected auth bit to be false for non-authoritative domain")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCaseInsensitiveResolveA(t *testing.T) {
|
||||||
|
resolv := resolver{server: "127.0.0.1:15353"}
|
||||||
|
answer, err := resolv.lookup("aUtH.eXAmpLe.org", dns.TypeA)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(answer.Answer) == 0 {
|
||||||
|
t.Error("No answer for DNS query")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCaseInsensitiveResolveSOA(t *testing.T) {
|
||||||
|
resolv := resolver{server: "127.0.0.1:15353"}
|
||||||
|
answer, _ := resolv.lookup("doesnotexist.aUtH.eXAmpLe.org", dns.TypeSOA)
|
||||||
|
if answer.Rcode != dns.RcodeNameError {
|
||||||
|
t.Errorf("Was expecing NXDOMAIN rcode, but got [%s] instead.", dns.RcodeToString[answer.Rcode])
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(answer.Ns) == 0 {
|
||||||
|
t.Error("No SOA answer for DNS query")
|
||||||
|
}
|
||||||
|
}
|
||||||
160
pkg/nameserver/handler.go
Normal file
160
pkg/nameserver/handler.go
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
package nameserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (n *Nameserver) handleRequest(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetReply(r)
|
||||||
|
// handle edns0
|
||||||
|
opt := r.IsEdns0()
|
||||||
|
if opt != nil {
|
||||||
|
if opt.Version() != 0 {
|
||||||
|
// Only EDNS0 is standardized
|
||||||
|
m.Rcode = dns.RcodeBadVers
|
||||||
|
m.SetEdns0(512, false)
|
||||||
|
} else {
|
||||||
|
// We can safely do this as we know that we're not setting other OPT RRs within acme-dns.
|
||||||
|
m.SetEdns0(512, false)
|
||||||
|
if r.Opcode == dns.OpcodeQuery {
|
||||||
|
n.readQuery(m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if r.Opcode == dns.OpcodeQuery {
|
||||||
|
n.readQuery(m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = w.WriteMsg(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Nameserver) readQuery(m *dns.Msg) {
|
||||||
|
var authoritative = false
|
||||||
|
for _, que := range m.Question {
|
||||||
|
if rr, rc, auth, err := n.answer(que); err == nil {
|
||||||
|
if auth {
|
||||||
|
authoritative = auth
|
||||||
|
}
|
||||||
|
m.Rcode = rc
|
||||||
|
m.Answer = append(m.Answer, rr...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.Authoritative = authoritative
|
||||||
|
if authoritative {
|
||||||
|
if m.Rcode == dns.RcodeNameError {
|
||||||
|
m.Ns = append(m.Ns, n.SOA)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Nameserver) answer(q dns.Question) ([]dns.RR, int, bool, error) {
|
||||||
|
var rcode int
|
||||||
|
var err error
|
||||||
|
var txtRRs []dns.RR
|
||||||
|
loweredName := strings.ToLower(q.Name)
|
||||||
|
var authoritative = n.isAuthoritative(loweredName)
|
||||||
|
if !n.isOwnChallenge(loweredName) && !n.answeringForDomain(loweredName) {
|
||||||
|
rcode = dns.RcodeNameError
|
||||||
|
}
|
||||||
|
r, _ := n.getRecord(loweredName, q.Qtype)
|
||||||
|
if q.Qtype == dns.TypeTXT {
|
||||||
|
if n.isOwnChallenge(loweredName) {
|
||||||
|
txtRRs, err = n.answerOwnChallenge(q)
|
||||||
|
} else {
|
||||||
|
txtRRs, err = n.answerTXT(q)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
r = append(r, txtRRs...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(r) > 0 {
|
||||||
|
// Make sure that we return NOERROR if there were dynamic records for the domain
|
||||||
|
rcode = dns.RcodeSuccess
|
||||||
|
}
|
||||||
|
n.Logger.Debugw("Answering question for domain",
|
||||||
|
"qtype", dns.TypeToString[q.Qtype],
|
||||||
|
"domain", q.Name,
|
||||||
|
"rcode", dns.RcodeToString[rcode])
|
||||||
|
return r, rcode, authoritative, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Nameserver) answerTXT(q dns.Question) ([]dns.RR, error) {
|
||||||
|
var ra []dns.RR
|
||||||
|
subdomain := sanitizeDomainQuestion(q.Name)
|
||||||
|
atxt, err := n.DB.GetTXTForDomain(subdomain)
|
||||||
|
if err != nil {
|
||||||
|
n.Logger.Errorw("Error while trying to get record",
|
||||||
|
"error", err.Error())
|
||||||
|
return ra, err
|
||||||
|
}
|
||||||
|
for _, v := range atxt {
|
||||||
|
if len(v) > 0 {
|
||||||
|
r := new(dns.TXT)
|
||||||
|
r.Hdr = dns.RR_Header{Name: q.Name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 1}
|
||||||
|
r.Txt = append(r.Txt, v)
|
||||||
|
ra = append(ra, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ra, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Nameserver) isAuthoritative(name string) bool {
|
||||||
|
if n.answeringForDomain(name) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
off := 0
|
||||||
|
for {
|
||||||
|
i, next := dns.NextLabel(name, off)
|
||||||
|
if next {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
off = i
|
||||||
|
if n.answeringForDomain(name[off:]) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Nameserver) isOwnChallenge(name string) bool {
|
||||||
|
if strings.HasPrefix(name, "_acme-challenge.") {
|
||||||
|
domain := name[16:]
|
||||||
|
if domain == n.OwnDomain {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// answeringForDomain checks if we have any records for a domain
|
||||||
|
func (n *Nameserver) answeringForDomain(name string) bool {
|
||||||
|
if n.OwnDomain == name {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
_, ok := n.Domains[name]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Nameserver) getRecord(name string, qtype uint16) ([]dns.RR, error) {
|
||||||
|
var rr []dns.RR
|
||||||
|
var cnames []dns.RR
|
||||||
|
domain, ok := n.Domains[name]
|
||||||
|
if !ok {
|
||||||
|
return rr, fmt.Errorf("no records for domain %s", name)
|
||||||
|
}
|
||||||
|
for _, ri := range domain.Records {
|
||||||
|
if ri.Header().Rrtype == qtype {
|
||||||
|
rr = append(rr, ri)
|
||||||
|
}
|
||||||
|
if ri.Header().Rrtype == dns.TypeCNAME {
|
||||||
|
cnames = append(cnames, ri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(rr) == 0 {
|
||||||
|
return cnames, nil
|
||||||
|
}
|
||||||
|
return rr, nil
|
||||||
|
}
|
||||||
150
pkg/nameserver/handler_test.go
Normal file
150
pkg/nameserver/handler_test.go
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
package nameserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNameserver_isOwnChallenge(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
OwnDomain string
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "is own challenge",
|
||||||
|
fields: fields{
|
||||||
|
OwnDomain: "some-domain.test.",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
name: "_acme-challenge.some-domain.test.",
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "challenge but not for us",
|
||||||
|
fields: fields{
|
||||||
|
OwnDomain: "some-domain.test.",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
name: "_acme-challenge.some-other-domain.test.",
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not a challenge",
|
||||||
|
fields: fields{
|
||||||
|
OwnDomain: "domain.test.",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
name: "domain.test.",
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "other request challenge",
|
||||||
|
fields: fields{
|
||||||
|
OwnDomain: "domain.test.",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
name: "my-domain.test.",
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
n := &Nameserver{
|
||||||
|
OwnDomain: tt.fields.OwnDomain,
|
||||||
|
}
|
||||||
|
if got := n.isOwnChallenge(tt.args.name); got != tt.want {
|
||||||
|
t.Errorf("isOwnChallenge() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNameserver_isAuthoritative(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
OwnDomain string
|
||||||
|
Domains map[string]Records
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
q dns.Question
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "is authoritative own domain",
|
||||||
|
fields: fields{
|
||||||
|
OwnDomain: "auth.domain.",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
q: dns.Question{Name: "auth.domain."},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "is authoritative other domain",
|
||||||
|
fields: fields{
|
||||||
|
OwnDomain: "auth.domain.",
|
||||||
|
Domains: map[string]Records{
|
||||||
|
"other-domain.test.": {Records: nil},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
q: dns.Question{Name: "other-domain.test."},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "is authoritative sub domain",
|
||||||
|
fields: fields{
|
||||||
|
OwnDomain: "auth.domain.",
|
||||||
|
Domains: map[string]Records{
|
||||||
|
"other-domain.test.": {Records: nil},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
q: dns.Question{Name: "sub.auth.domain."},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "is not authoritative own",
|
||||||
|
fields: fields{
|
||||||
|
OwnDomain: "auth.domain.",
|
||||||
|
Domains: map[string]Records{
|
||||||
|
"other-domain.test.": {Records: nil},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
q: dns.Question{Name: "special-auth.domain."},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
n := &Nameserver{
|
||||||
|
OwnDomain: tt.fields.OwnDomain,
|
||||||
|
Domains: tt.fields.Domains,
|
||||||
|
}
|
||||||
|
if got := n.isAuthoritative(tt.args.q.Name); got != tt.want {
|
||||||
|
t.Errorf("isAuthoritative() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
106
pkg/nameserver/initialize.go
Normal file
106
pkg/nameserver/initialize.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
package nameserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/joohoi/acme-dns/pkg/acmedns"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Records is a slice of ResourceRecords
|
||||||
|
type Records struct {
|
||||||
|
Records []dns.RR
|
||||||
|
}
|
||||||
|
|
||||||
|
type Nameserver struct {
|
||||||
|
Config *acmedns.AcmeDnsConfig
|
||||||
|
DB acmedns.AcmednsDB
|
||||||
|
Logger *zap.SugaredLogger
|
||||||
|
Server *dns.Server
|
||||||
|
OwnDomain string
|
||||||
|
NotifyStartedFunc func()
|
||||||
|
SOA dns.RR
|
||||||
|
mu sync.RWMutex
|
||||||
|
personalAuthKey string
|
||||||
|
Domains map[string]Records
|
||||||
|
errChan chan error
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitAndStart(config *acmedns.AcmeDnsConfig, db acmedns.AcmednsDB, logger *zap.SugaredLogger, errChan chan error) []acmedns.AcmednsNS {
|
||||||
|
dnsservers := make([]acmedns.AcmednsNS, 0)
|
||||||
|
waitLock := sync.Mutex{}
|
||||||
|
if strings.HasPrefix(config.General.Proto, "both") {
|
||||||
|
|
||||||
|
// Handle the case where DNS server should be started for both udp and tcp
|
||||||
|
udpProto := "udp"
|
||||||
|
tcpProto := "tcp"
|
||||||
|
if strings.HasSuffix(config.General.Proto, "4") {
|
||||||
|
udpProto += "4"
|
||||||
|
tcpProto += "4"
|
||||||
|
} else if strings.HasSuffix(config.General.Proto, "6") {
|
||||||
|
udpProto += "6"
|
||||||
|
tcpProto += "6"
|
||||||
|
}
|
||||||
|
dnsServerUDP := NewDNSServer(config, db, logger, udpProto)
|
||||||
|
dnsservers = append(dnsservers, dnsServerUDP)
|
||||||
|
dnsServerUDP.ParseRecords()
|
||||||
|
dnsServerTCP := NewDNSServer(config, db, logger, tcpProto)
|
||||||
|
dnsservers = append(dnsservers, dnsServerTCP)
|
||||||
|
dnsServerTCP.ParseRecords()
|
||||||
|
// wait for the server to get started to proceed
|
||||||
|
waitLock.Lock()
|
||||||
|
dnsServerUDP.SetNotifyStartedFunc(waitLock.Unlock)
|
||||||
|
go dnsServerUDP.Start(errChan)
|
||||||
|
waitLock.Lock()
|
||||||
|
dnsServerTCP.SetNotifyStartedFunc(waitLock.Unlock)
|
||||||
|
go dnsServerTCP.Start(errChan)
|
||||||
|
waitLock.Lock()
|
||||||
|
} else {
|
||||||
|
dnsServer := NewDNSServer(config, db, logger, config.General.Proto)
|
||||||
|
dnsservers = append(dnsservers, dnsServer)
|
||||||
|
dnsServer.ParseRecords()
|
||||||
|
waitLock.Lock()
|
||||||
|
dnsServer.SetNotifyStartedFunc(waitLock.Unlock)
|
||||||
|
go dnsServer.Start(errChan)
|
||||||
|
waitLock.Lock()
|
||||||
|
}
|
||||||
|
return dnsservers
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDNSServer parses the DNS records from config and returns a new DNSServer struct
|
||||||
|
func NewDNSServer(config *acmedns.AcmeDnsConfig, db acmedns.AcmednsDB, logger *zap.SugaredLogger, proto string) acmedns.AcmednsNS {
|
||||||
|
// dnsServerTCP := NewDNSServer(DB, Config.General.Listen, tcpProto, Config.General.Domain)
|
||||||
|
server := Nameserver{Config: config, DB: db, Logger: logger}
|
||||||
|
server.Server = &dns.Server{Addr: config.General.Listen, Net: proto}
|
||||||
|
domain := config.General.Domain
|
||||||
|
if !strings.HasSuffix(domain, ".") {
|
||||||
|
domain = domain + "."
|
||||||
|
}
|
||||||
|
server.OwnDomain = strings.ToLower(domain)
|
||||||
|
server.personalAuthKey = ""
|
||||||
|
server.Domains = make(map[string]Records)
|
||||||
|
return &server
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Nameserver) Start(errorChannel chan error) {
|
||||||
|
n.errChan = errorChannel
|
||||||
|
dns.HandleFunc(".", n.handleRequest)
|
||||||
|
n.Logger.Infow("Starting DNS listener",
|
||||||
|
"addr", n.Server.Addr,
|
||||||
|
"proto", n.Server.Net)
|
||||||
|
if n.NotifyStartedFunc != nil {
|
||||||
|
n.Server.NotifyStartedFunc = n.NotifyStartedFunc
|
||||||
|
}
|
||||||
|
err := n.Server.ListenAndServe()
|
||||||
|
if err != nil {
|
||||||
|
errorChannel <- fmt.Errorf("DNS server %s failed: %w", n.Server.Net, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Nameserver) SetNotifyStartedFunc(fun func()) {
|
||||||
|
n.Server.NotifyStartedFunc = fun
|
||||||
|
}
|
||||||
52
pkg/nameserver/parseconfig.go
Normal file
52
pkg/nameserver/parseconfig.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package nameserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseRecords parses a slice of DNS record string
|
||||||
|
func (n *Nameserver) ParseRecords() {
|
||||||
|
for _, v := range n.Config.General.StaticRecords {
|
||||||
|
rr, err := dns.NewRR(strings.ToLower(v))
|
||||||
|
if err != nil {
|
||||||
|
n.Logger.Errorw("Could not parse RR from config",
|
||||||
|
"error", err.Error(),
|
||||||
|
"rr", v)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Add parsed RR
|
||||||
|
n.appendRR(rr)
|
||||||
|
}
|
||||||
|
// Create serial
|
||||||
|
serial := time.Now().Format("2006010215")
|
||||||
|
// Add SOA
|
||||||
|
SOAstring := fmt.Sprintf("%s. SOA %s. %s. %s 28800 7200 604800 86400", strings.ToLower(n.Config.General.Domain), strings.ToLower(n.Config.General.Nsname), strings.ToLower(n.Config.General.Nsadmin), serial)
|
||||||
|
soarr, err := dns.NewRR(SOAstring)
|
||||||
|
if err != nil {
|
||||||
|
n.Logger.Errorw("Error while adding SOA record",
|
||||||
|
"error", err.Error(),
|
||||||
|
"soa", SOAstring)
|
||||||
|
} else {
|
||||||
|
n.appendRR(soarr)
|
||||||
|
n.SOA = soarr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Nameserver) appendRR(rr dns.RR) {
|
||||||
|
addDomain := rr.Header().Name
|
||||||
|
_, ok := n.Domains[addDomain]
|
||||||
|
if !ok {
|
||||||
|
n.Domains[addDomain] = Records{[]dns.RR{rr}}
|
||||||
|
} else {
|
||||||
|
drecs := n.Domains[addDomain]
|
||||||
|
drecs.Records = append(drecs.Records, rr)
|
||||||
|
n.Domains[addDomain] = drecs
|
||||||
|
}
|
||||||
|
n.Logger.Debugw("Adding new record to domain",
|
||||||
|
"recordtype", dns.TypeToString[rr.Header().Rrtype],
|
||||||
|
"domain", addDomain)
|
||||||
|
}
|
||||||
12
pkg/nameserver/util.go
Normal file
12
pkg/nameserver/util.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package nameserver
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
func sanitizeDomainQuestion(d string) string {
|
||||||
|
dom := strings.ToLower(d)
|
||||||
|
firstDot := strings.Index(d, ".")
|
||||||
|
if firstDot > 0 {
|
||||||
|
dom = dom[0:firstDot]
|
||||||
|
}
|
||||||
|
return dom
|
||||||
|
}
|
||||||
20
pkg/nameserver/validation.go
Normal file
20
pkg/nameserver/validation.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package nameserver
|
||||||
|
|
||||||
|
import "github.com/miekg/dns"
|
||||||
|
|
||||||
|
// SetOwnAuthKey sets the ACME challenge token for completing dns validation for acme-dns server itself
|
||||||
|
func (n *Nameserver) SetOwnAuthKey(key string) {
|
||||||
|
n.mu.Lock()
|
||||||
|
defer n.mu.Unlock()
|
||||||
|
n.personalAuthKey = key
|
||||||
|
}
|
||||||
|
|
||||||
|
// answerOwnChallenge answers to ACME challenge for acme-dns own certificate
|
||||||
|
func (n *Nameserver) answerOwnChallenge(q dns.Question) ([]dns.RR, error) {
|
||||||
|
n.mu.RLock()
|
||||||
|
defer n.mu.RUnlock()
|
||||||
|
r := new(dns.TXT)
|
||||||
|
r.Hdr = dns.RR_Header{Name: q.Name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 1}
|
||||||
|
r.Txt = append(r.Txt, n.personalAuthKey)
|
||||||
|
return []dns.RR{r}, nil
|
||||||
|
}
|
||||||
65
pkg/nameserver/validation_test.go
Normal file
65
pkg/nameserver/validation_test.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package nameserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNameserver_answerOwnChallenge(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
personalAuthKey string
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
q dns.Question
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
want []dns.RR
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "answer own challenge",
|
||||||
|
fields: fields{
|
||||||
|
personalAuthKey: "some key text",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
q: dns.Question{
|
||||||
|
Name: "something",
|
||||||
|
Qtype: 0,
|
||||||
|
Qclass: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []dns.RR{
|
||||||
|
&dns.TXT{
|
||||||
|
Hdr: dns.RR_Header{Name: "something", Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 1},
|
||||||
|
Txt: []string{"some key text"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
n := &Nameserver{}
|
||||||
|
|
||||||
|
n.SetOwnAuthKey(tt.fields.personalAuthKey)
|
||||||
|
if n.personalAuthKey != tt.fields.personalAuthKey {
|
||||||
|
t.Errorf("failed to set personal auth key: got = %s, want %s", n.personalAuthKey, tt.fields.personalAuthKey)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := n.answerOwnChallenge(tt.args.q)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("answerOwnChallenge() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("answerOwnChallenge() got = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
37
test/README.md
Normal file
37
test/README.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# acme-dns E2E Testing Suite
|
||||||
|
|
||||||
|
This directory contains the end-to-end (E2E) testing suite for `acme-dns`. The suite runs in a containerized environment to ensure a consistent and isolated test execution.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The E2E suite consists of:
|
||||||
|
- A Dockerized `acme-dns` server.
|
||||||
|
- A Python-based `tester` container that performs API and DNS operations.
|
||||||
|
- A GitHub Actions workflow for CI/CD integration.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- [Docker](https://www.docker.com/get-started)
|
||||||
|
- [Docker Compose](https://docs.docker.com/compose/install/)
|
||||||
|
|
||||||
|
## Running Locally
|
||||||
|
|
||||||
|
To run the full E2E suite locally, execute the following command from the root of the repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f test/e2e/docker-compose.yml up --build --abort-on-container-exit
|
||||||
|
```
|
||||||
|
|
||||||
|
The `tester` container will return an exit code of `0` on success and `1` on failure.
|
||||||
|
|
||||||
|
## Test Flow
|
||||||
|
|
||||||
|
The `tester.py` script follows these steps:
|
||||||
|
1. **Wait for Ready**: Polls the `/health` endpoint until the API is available.
|
||||||
|
2. **Account Registration**: Registers a NEW account at `/register`.
|
||||||
|
3. **TXT Update**: Performs TWO sequential updates to the TXT records of the newly created subdomain.
|
||||||
|
4. **DNS Verification**: Directly queries the `acme-dns` server (on port 53) used in the test to verify that the TXT records have been correctly updated and are resolvable.
|
||||||
|
|
||||||
|
## CI/CD integration
|
||||||
|
|
||||||
|
The tests are automatically run on every push and pull request to the `master` branch via GitHub Actions. The workflow configuration can be found in `.github/workflows/e2e.yml`.
|
||||||
16
test/e2e/Dockerfile.e2e
Normal file
16
test/e2e/Dockerfile.e2e
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
FROM golang:alpine AS builder
|
||||||
|
RUN apk add --update gcc musl-dev git
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . .
|
||||||
|
RUN CGO_ENABLED=1 go build -o acme-dns
|
||||||
|
|
||||||
|
FROM alpine:latest
|
||||||
|
WORKDIR /root/
|
||||||
|
COPY --from=builder /app/acme-dns .
|
||||||
|
COPY --from=builder /app/config.cfg /etc/acme-dns/config.cfg
|
||||||
|
RUN mkdir -p /var/lib/acme-dns
|
||||||
|
RUN apk --no-cache add ca-certificates && update-ca-certificates
|
||||||
|
VOLUME ["/etc/acme-dns", "/var/lib/acme-dns"]
|
||||||
|
ENTRYPOINT ["./acme-dns", "-c", "/etc/acme-dns/config.cfg"]
|
||||||
|
EXPOSE 53 80 443
|
||||||
|
EXPOSE 53/udp
|
||||||
28
test/e2e/config.cfg
Normal file
28
test/e2e/config.cfg
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
[general]
|
||||||
|
listen = "0.0.0.0:53"
|
||||||
|
protocol = "both"
|
||||||
|
domain = "auth.example.org"
|
||||||
|
nsname = "auth.example.org"
|
||||||
|
nsadmin = "admin.example.org"
|
||||||
|
records = [
|
||||||
|
"auth.example.org. A 127.0.0.1",
|
||||||
|
"auth.example.org. NS auth.example.org.",
|
||||||
|
]
|
||||||
|
debug = true
|
||||||
|
|
||||||
|
[database]
|
||||||
|
engine = "sqlite"
|
||||||
|
connection = "/var/lib/acme-dns/acme-dns.db"
|
||||||
|
|
||||||
|
[api]
|
||||||
|
ip = "0.0.0.0"
|
||||||
|
disable_registration = false
|
||||||
|
port = "80"
|
||||||
|
tls = "none"
|
||||||
|
corsorigins = ["*"]
|
||||||
|
use_header = false
|
||||||
|
|
||||||
|
[logconfig]
|
||||||
|
loglevel = "debug"
|
||||||
|
logtype = "stdout"
|
||||||
|
logformat = "text"
|
||||||
30
test/e2e/docker-compose.yml
Normal file
30
test/e2e/docker-compose.yml
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
acme-dns:
|
||||||
|
build:
|
||||||
|
context: ../../
|
||||||
|
dockerfile: test/e2e/Dockerfile.e2e
|
||||||
|
ports:
|
||||||
|
- "15353:53/udp"
|
||||||
|
- "18080:80"
|
||||||
|
- "18443:443"
|
||||||
|
volumes:
|
||||||
|
- ./config.cfg:/etc/acme-dns/config.cfg
|
||||||
|
- e2e-data:/var/lib/acme-dns
|
||||||
|
|
||||||
|
tester:
|
||||||
|
image: python:3.9-slim
|
||||||
|
depends_on:
|
||||||
|
- acme-dns
|
||||||
|
volumes:
|
||||||
|
- .:/test
|
||||||
|
working_dir: /test
|
||||||
|
command: >
|
||||||
|
sh -c "pip install -r requirements.txt && python tester.py"
|
||||||
|
environment:
|
||||||
|
- ACMEDNS_URL=http://acme-dns:80
|
||||||
|
- DNS_SERVER=acme-dns
|
||||||
|
- DNS_PORT=53
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
e2e-data:
|
||||||
2
test/e2e/requirements.txt
Normal file
2
test/e2e/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
requests
|
||||||
|
dnspython
|
||||||
105
test/e2e/tester.py
Normal file
105
test/e2e/tester.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import requests
|
||||||
|
import dns.resolver
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import socket
|
||||||
|
|
||||||
|
ACMEDNS_URL = os.environ.get("ACMEDNS_URL", "http://localhost:80")
|
||||||
|
DNS_SERVER = os.environ.get("DNS_SERVER", "localhost")
|
||||||
|
DNS_PORT = int(os.environ.get("DNS_PORT", 53))
|
||||||
|
|
||||||
|
def wait_for_server():
|
||||||
|
print(f"Waiting for acme-dns at {ACMEDNS_URL}...")
|
||||||
|
for i in range(30):
|
||||||
|
try:
|
||||||
|
resp = requests.get(f"{ACMEDNS_URL}/health")
|
||||||
|
if resp.status_code == 200:
|
||||||
|
print("Server is up!")
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
time.sleep(1)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_flow():
|
||||||
|
# 1. Register account
|
||||||
|
print("Registering account...")
|
||||||
|
resp = requests.post(f"{ACMEDNS_URL}/register")
|
||||||
|
if resp.status_code != 201:
|
||||||
|
print(f"Failed to register: {resp.status_code} {resp.text}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
account = resp.json()
|
||||||
|
username = account['username']
|
||||||
|
api_key = account['password']
|
||||||
|
subdomain = account['subdomain']
|
||||||
|
fulldomain = account['fulldomain']
|
||||||
|
print(f"Registered subdomain: {subdomain}")
|
||||||
|
|
||||||
|
# 2. Update TXT records
|
||||||
|
headers = {
|
||||||
|
"X-Api-User": username,
|
||||||
|
"X-Api-Key": api_key
|
||||||
|
}
|
||||||
|
|
||||||
|
txt_values = ["secret_token_1", "secret_token_2"]
|
||||||
|
|
||||||
|
for val in txt_values:
|
||||||
|
print(f"Updating TXT record with value: {val}")
|
||||||
|
# Let's Encrypt uses 43 char tokens usually, but our validation is flexible now (or we use a dummy one)
|
||||||
|
# Actually our current validation in pkg/api/util.go still expects 43 chars if I recall correctly
|
||||||
|
# Let's use 43 chars just in case
|
||||||
|
dummy_val = val.ljust(43, '_')[:43]
|
||||||
|
payload = {
|
||||||
|
"subdomain": subdomain,
|
||||||
|
"txt": dummy_val
|
||||||
|
}
|
||||||
|
resp = requests.post(f"{ACMEDNS_URL}/update", headers=headers, json=payload)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
print(f"Failed to update: {resp.status_code} {resp.text}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print("Updates successful. Waiting for DNS propagation (local cache)...")
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# 3. Verify DNS resolution
|
||||||
|
print(f"Resolving TXT records for {fulldomain}...")
|
||||||
|
|
||||||
|
# Resolve hostname to IP if needed
|
||||||
|
try:
|
||||||
|
dns_server_ip = socket.gethostbyname(DNS_SERVER)
|
||||||
|
except:
|
||||||
|
dns_server_ip = DNS_SERVER
|
||||||
|
|
||||||
|
resolver = dns.resolver.Resolver()
|
||||||
|
resolver.nameservers = [dns_server_ip]
|
||||||
|
resolver.port = DNS_PORT
|
||||||
|
|
||||||
|
try:
|
||||||
|
answers = resolver.resolve(fulldomain, "TXT")
|
||||||
|
resolved_values = [str(rdata).strip('"') for rdata in answers]
|
||||||
|
print(f"Resolved values: {resolved_values}")
|
||||||
|
|
||||||
|
# Check if both are present
|
||||||
|
for val in txt_values:
|
||||||
|
dummy_val = val.ljust(43, '_')[:43]
|
||||||
|
if dummy_val not in resolved_values:
|
||||||
|
print(f"Expected value {dummy_val} not found in resolved values")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"DNS resolution failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print("E2E Test Passed Successfully!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if not wait_for_server():
|
||||||
|
print("Server timed out.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not test_flow():
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
sys.exit(0)
|
||||||
89
types.go
89
types.go
@ -1,89 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
"github.com/satori/go.uuid"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Config is global configuration struct
|
|
||||||
var Config DNSConfig
|
|
||||||
|
|
||||||
// DB is used to access the database functions in acme-dns
|
|
||||||
var DB database
|
|
||||||
|
|
||||||
// RR holds the static DNS records
|
|
||||||
var RR Records
|
|
||||||
|
|
||||||
// Records is for static records
|
|
||||||
type Records struct {
|
|
||||||
Records map[uint16]map[string][]dns.RR
|
|
||||||
}
|
|
||||||
|
|
||||||
// DNSConfig holds the config structure
|
|
||||||
type DNSConfig struct {
|
|
||||||
General general
|
|
||||||
Database dbsettings
|
|
||||||
API httpapi
|
|
||||||
Logconfig logconfig
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auth middleware
|
|
||||||
type authMiddleware struct{}
|
|
||||||
|
|
||||||
// Config file general section
|
|
||||||
type general struct {
|
|
||||||
Listen string
|
|
||||||
Proto string `toml:"protocol"`
|
|
||||||
Domain string
|
|
||||||
Nsname string
|
|
||||||
Nsadmin string
|
|
||||||
Debug bool
|
|
||||||
StaticRecords []string `toml:"records"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type dbsettings struct {
|
|
||||||
Engine string
|
|
||||||
Connection string
|
|
||||||
}
|
|
||||||
|
|
||||||
// API config
|
|
||||||
type httpapi struct {
|
|
||||||
Domain string `toml:"api_domain"`
|
|
||||||
IP string
|
|
||||||
AutocertPort string `toml:"autocert_port"`
|
|
||||||
Port string `toml:"port"`
|
|
||||||
TLS string
|
|
||||||
TLSCertPrivkey string `toml:"tls_cert_privkey"`
|
|
||||||
TLSCertFullchain string `toml:"tls_cert_fullchain"`
|
|
||||||
CorsOrigins []string
|
|
||||||
UseHeader bool `toml:"use_header"`
|
|
||||||
HeaderName string `toml:"header_name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logging config
|
|
||||||
type logconfig struct {
|
|
||||||
Level string `toml:"loglevel"`
|
|
||||||
Logtype string `toml:"logtype"`
|
|
||||||
File string `toml:"logfile"`
|
|
||||||
Format string `toml:"logformat"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type acmedb struct {
|
|
||||||
sync.Mutex
|
|
||||||
DB *sql.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
type database interface {
|
|
||||||
Init(string, string) error
|
|
||||||
Register(cidrslice) (ACMETxt, error)
|
|
||||||
GetByUsername(uuid.UUID) (ACMETxt, error)
|
|
||||||
GetTXTForDomain(string) ([]string, error)
|
|
||||||
Update(ACMETxt) error
|
|
||||||
GetBackend() *sql.DB
|
|
||||||
SetBackend(*sql.DB)
|
|
||||||
Close()
|
|
||||||
Lock()
|
|
||||||
Unlock()
|
|
||||||
}
|
|
||||||
9
umask_unix.go
Normal file
9
umask_unix.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "syscall"
|
||||||
|
|
||||||
|
func setUmask() {
|
||||||
|
syscall.Umask(0077)
|
||||||
|
}
|
||||||
7
umask_windows.go
Normal file
7
umask_windows.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
func setUmask() {
|
||||||
|
// umask is not supported on Windows
|
||||||
|
}
|
||||||
96
util.go
96
util.go
@ -1,96 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"os"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/BurntSushi/toml"
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
func jsonError(message string) []byte {
|
|
||||||
return []byte(fmt.Sprintf("{\"error\": \"%s\"}", message))
|
|
||||||
}
|
|
||||||
|
|
||||||
func fileExists(fname string) bool {
|
|
||||||
_, err := os.Stat(fname)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func readConfig(fname string) DNSConfig {
|
|
||||||
var conf DNSConfig
|
|
||||||
// Practically never errors
|
|
||||||
_, _ = toml.DecodeFile(fname, &conf)
|
|
||||||
return conf
|
|
||||||
}
|
|
||||||
|
|
||||||
func sanitizeString(s string) string {
|
|
||||||
// URL safe base64 alphabet without padding as defined in ACME
|
|
||||||
re, _ := regexp.Compile("[^A-Za-z\\-\\_0-9]+")
|
|
||||||
return re.ReplaceAllString(s, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func generatePassword(length int) string {
|
|
||||||
ret := make([]byte, length)
|
|
||||||
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890-_"
|
|
||||||
alphalen := big.NewInt(int64(len(alphabet)))
|
|
||||||
for i := 0; i < length; i++ {
|
|
||||||
c, _ := rand.Int(rand.Reader, alphalen)
|
|
||||||
r := int(c.Int64())
|
|
||||||
ret[i] = alphabet[r]
|
|
||||||
}
|
|
||||||
return string(ret)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sanitizeDomainQuestion(d string) string {
|
|
||||||
dom := strings.ToLower(d)
|
|
||||||
firstDot := strings.Index(d, ".")
|
|
||||||
if firstDot > 0 {
|
|
||||||
dom = dom[0:firstDot]
|
|
||||||
}
|
|
||||||
return dom
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupLogging(format string, level string) {
|
|
||||||
if format == "json" {
|
|
||||||
log.SetFormatter(&log.JSONFormatter{})
|
|
||||||
}
|
|
||||||
switch level {
|
|
||||||
default:
|
|
||||||
log.SetLevel(log.WarnLevel)
|
|
||||||
case "debug":
|
|
||||||
log.SetLevel(log.DebugLevel)
|
|
||||||
case "info":
|
|
||||||
log.SetLevel(log.InfoLevel)
|
|
||||||
case "error":
|
|
||||||
log.SetLevel(log.ErrorLevel)
|
|
||||||
}
|
|
||||||
// TODO: file logging
|
|
||||||
}
|
|
||||||
|
|
||||||
func startDNS(listen string, proto string) *dns.Server {
|
|
||||||
// DNS server part
|
|
||||||
dns.HandleFunc(".", handleRequest)
|
|
||||||
server := &dns.Server{Addr: listen, Net: proto}
|
|
||||||
go server.ListenAndServe()
|
|
||||||
return server
|
|
||||||
}
|
|
||||||
|
|
||||||
func getIPListFromHeader(header string) []string {
|
|
||||||
iplist := []string{}
|
|
||||||
for _, v := range strings.Split(header, ",") {
|
|
||||||
if len(v) > 0 {
|
|
||||||
// Ignore empty values
|
|
||||||
iplist = append(iplist, strings.TrimSpace(v))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return iplist
|
|
||||||
}
|
|
||||||
97
util_test.go
97
util_test.go
@ -1,97 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSetupLogging(t *testing.T) {
|
|
||||||
for i, test := range []struct {
|
|
||||||
format string
|
|
||||||
level string
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{"text", "warning", "warning"},
|
|
||||||
{"json", "debug", "debug"},
|
|
||||||
{"text", "info", "info"},
|
|
||||||
{"json", "error", "error"},
|
|
||||||
{"text", "something", "warning"},
|
|
||||||
} {
|
|
||||||
setupLogging(test.format, test.level)
|
|
||||||
if log.GetLevel().String() != test.expected {
|
|
||||||
t.Errorf("Test %d: Expected loglevel %s but got %s", i, test.expected, log.GetLevel().String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReadConfig(t *testing.T) {
|
|
||||||
for i, test := range []struct {
|
|
||||||
inFile []byte
|
|
||||||
output DNSConfig
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
[]byte("[general]\nlisten = \":53\"\ndebug = true\n[api]\napi_domain = \"something.strange\""),
|
|
||||||
DNSConfig{
|
|
||||||
General: general{
|
|
||||||
Listen: ":53",
|
|
||||||
Debug: true,
|
|
||||||
},
|
|
||||||
API: httpapi{
|
|
||||||
Domain: "something.strange",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
[]byte("[\x00[[[[[[[[[de\nlisten =]"),
|
|
||||||
DNSConfig{},
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
tmpfile, err := ioutil.TempFile("", "acmedns")
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Could not create temporary file")
|
|
||||||
}
|
|
||||||
defer os.Remove(tmpfile.Name())
|
|
||||||
|
|
||||||
if _, err := tmpfile.Write(test.inFile); err != nil {
|
|
||||||
t.Error("Could not write to temporary file")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tmpfile.Close(); err != nil {
|
|
||||||
t.Error("Could not close temporary file")
|
|
||||||
}
|
|
||||||
ret := readConfig(tmpfile.Name())
|
|
||||||
if ret.General.Listen != test.output.General.Listen {
|
|
||||||
t.Errorf("Test %d: Expected listen value %s, but got %s", i, test.output.General.Listen, ret.General.Listen)
|
|
||||||
}
|
|
||||||
if ret.API.Domain != test.output.API.Domain {
|
|
||||||
t.Errorf("Test %d: Expected HTTP API domain %s, but got %s", i, test.output.API.Domain, ret.API.Domain)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetIPListFromHeader(t *testing.T) {
|
|
||||||
for i, test := range []struct {
|
|
||||||
input string
|
|
||||||
output []string
|
|
||||||
}{
|
|
||||||
{"1.1.1.1, 2.2.2.2", []string{"1.1.1.1", "2.2.2.2"}},
|
|
||||||
{" 1.1.1.1 , 2.2.2.2", []string{"1.1.1.1", "2.2.2.2"}},
|
|
||||||
{",1.1.1.1 ,2.2.2.2", []string{"1.1.1.1", "2.2.2.2"}},
|
|
||||||
} {
|
|
||||||
res := getIPListFromHeader(test.input)
|
|
||||||
if len(res) != len(test.output) {
|
|
||||||
t.Errorf("Test %d: Expected [%d] items in return list, but got [%d]", i, len(test.output), len(res))
|
|
||||||
} else {
|
|
||||||
|
|
||||||
for j, vv := range test.output {
|
|
||||||
if res[j] != vv {
|
|
||||||
t.Errorf("Test %d: Expected return value [%v] but got [%v]", j, test.output, res)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"unicode/utf8"
|
|
||||||
|
|
||||||
"github.com/satori/go.uuid"
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
)
|
|
||||||
|
|
||||||
func getValidUsername(u string) (uuid.UUID, error) {
|
|
||||||
uname, err := uuid.FromString(u)
|
|
||||||
if err != nil {
|
|
||||||
return uuid.UUID{}, err
|
|
||||||
}
|
|
||||||
return uname, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validKey(k string) bool {
|
|
||||||
kn := sanitizeString(k)
|
|
||||||
if utf8.RuneCountInString(k) == 40 && utf8.RuneCountInString(kn) == 40 {
|
|
||||||
// Correct length and all chars valid
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func validSubdomain(s string) bool {
|
|
||||||
_, err := uuid.FromString(s)
|
|
||||||
if err == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func validTXT(s string) bool {
|
|
||||||
sn := sanitizeString(s)
|
|
||||||
if utf8.RuneCountInString(s) == 43 && utf8.RuneCountInString(sn) == 43 {
|
|
||||||
// 43 chars is the current LE auth key size, but not limited / defined by ACME
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func correctPassword(pw string, hash string) bool {
|
|
||||||
if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(pw)); err == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
5
vendor/github.com/BurntSushi/toml/.gitignore
generated
vendored
5
vendor/github.com/BurntSushi/toml/.gitignore
generated
vendored
@ -1,5 +0,0 @@
|
|||||||
TAGS
|
|
||||||
tags
|
|
||||||
.*.swp
|
|
||||||
tomlcheck/tomlcheck
|
|
||||||
toml.test
|
|
||||||
15
vendor/github.com/BurntSushi/toml/.travis.yml
generated
vendored
15
vendor/github.com/BurntSushi/toml/.travis.yml
generated
vendored
@ -1,15 +0,0 @@
|
|||||||
language: go
|
|
||||||
go:
|
|
||||||
- 1.1
|
|
||||||
- 1.2
|
|
||||||
- 1.3
|
|
||||||
- 1.4
|
|
||||||
- 1.5
|
|
||||||
- 1.6
|
|
||||||
- tip
|
|
||||||
install:
|
|
||||||
- go install ./...
|
|
||||||
- go get github.com/BurntSushi/toml-test
|
|
||||||
script:
|
|
||||||
- export PATH="$PATH:$HOME/gopath/bin"
|
|
||||||
- make test
|
|
||||||
3
vendor/github.com/BurntSushi/toml/COMPATIBLE
generated
vendored
3
vendor/github.com/BurntSushi/toml/COMPATIBLE
generated
vendored
@ -1,3 +0,0 @@
|
|||||||
Compatible with TOML version
|
|
||||||
[v0.4.0](https://github.com/toml-lang/toml/blob/v0.4.0/versions/en/toml-v0.4.0.md)
|
|
||||||
|
|
||||||
14
vendor/github.com/BurntSushi/toml/COPYING
generated
vendored
14
vendor/github.com/BurntSushi/toml/COPYING
generated
vendored
@ -1,14 +0,0 @@
|
|||||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
||||||
Version 2, December 2004
|
|
||||||
|
|
||||||
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
|
||||||
|
|
||||||
Everyone is permitted to copy and distribute verbatim or modified
|
|
||||||
copies of this license document, and changing it is allowed as long
|
|
||||||
as the name is changed.
|
|
||||||
|
|
||||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
||||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
|
||||||
|
|
||||||
0. You just DO WHAT THE FUCK YOU WANT TO.
|
|
||||||
|
|
||||||
19
vendor/github.com/BurntSushi/toml/Makefile
generated
vendored
19
vendor/github.com/BurntSushi/toml/Makefile
generated
vendored
@ -1,19 +0,0 @@
|
|||||||
install:
|
|
||||||
go install ./...
|
|
||||||
|
|
||||||
test: install
|
|
||||||
go test -v
|
|
||||||
toml-test toml-test-decoder
|
|
||||||
toml-test -encoder toml-test-encoder
|
|
||||||
|
|
||||||
fmt:
|
|
||||||
gofmt -w *.go */*.go
|
|
||||||
colcheck *.go */*.go
|
|
||||||
|
|
||||||
tags:
|
|
||||||
find ./ -name '*.go' -print0 | xargs -0 gotags > TAGS
|
|
||||||
|
|
||||||
push:
|
|
||||||
git push origin master
|
|
||||||
git push github master
|
|
||||||
|
|
||||||
218
vendor/github.com/BurntSushi/toml/README.md
generated
vendored
218
vendor/github.com/BurntSushi/toml/README.md
generated
vendored
@ -1,218 +0,0 @@
|
|||||||
## TOML parser and encoder for Go with reflection
|
|
||||||
|
|
||||||
TOML stands for Tom's Obvious, Minimal Language. This Go package provides a
|
|
||||||
reflection interface similar to Go's standard library `json` and `xml`
|
|
||||||
packages. This package also supports the `encoding.TextUnmarshaler` and
|
|
||||||
`encoding.TextMarshaler` interfaces so that you can define custom data
|
|
||||||
representations. (There is an example of this below.)
|
|
||||||
|
|
||||||
Spec: https://github.com/toml-lang/toml
|
|
||||||
|
|
||||||
Compatible with TOML version
|
|
||||||
[v0.4.0](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md)
|
|
||||||
|
|
||||||
Documentation: https://godoc.org/github.com/BurntSushi/toml
|
|
||||||
|
|
||||||
Installation:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go get github.com/BurntSushi/toml
|
|
||||||
```
|
|
||||||
|
|
||||||
Try the toml validator:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go get github.com/BurntSushi/toml/cmd/tomlv
|
|
||||||
tomlv some-toml-file.toml
|
|
||||||
```
|
|
||||||
|
|
||||||
[](https://travis-ci.org/BurntSushi/toml) [](https://godoc.org/github.com/BurntSushi/toml)
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
|
|
||||||
This package passes all tests in
|
|
||||||
[toml-test](https://github.com/BurntSushi/toml-test) for both the decoder
|
|
||||||
and the encoder.
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
This package works similarly to how the Go standard library handles `XML`
|
|
||||||
and `JSON`. Namely, data is loaded into Go values via reflection.
|
|
||||||
|
|
||||||
For the simplest example, consider some TOML file as just a list of keys
|
|
||||||
and values:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
Age = 25
|
|
||||||
Cats = [ "Cauchy", "Plato" ]
|
|
||||||
Pi = 3.14
|
|
||||||
Perfection = [ 6, 28, 496, 8128 ]
|
|
||||||
DOB = 1987-07-05T05:45:00Z
|
|
||||||
```
|
|
||||||
|
|
||||||
Which could be defined in Go as:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type Config struct {
|
|
||||||
Age int
|
|
||||||
Cats []string
|
|
||||||
Pi float64
|
|
||||||
Perfection []int
|
|
||||||
DOB time.Time // requires `import time`
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
And then decoded with:
|
|
||||||
|
|
||||||
```go
|
|
||||||
var conf Config
|
|
||||||
if _, err := toml.Decode(tomlData, &conf); err != nil {
|
|
||||||
// handle error
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also use struct tags if your struct field name doesn't map to a TOML
|
|
||||||
key value directly:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
some_key_NAME = "wat"
|
|
||||||
```
|
|
||||||
|
|
||||||
```go
|
|
||||||
type TOML struct {
|
|
||||||
ObscureKey string `toml:"some_key_NAME"`
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using the `encoding.TextUnmarshaler` interface
|
|
||||||
|
|
||||||
Here's an example that automatically parses duration strings into
|
|
||||||
`time.Duration` values:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[[song]]
|
|
||||||
name = "Thunder Road"
|
|
||||||
duration = "4m49s"
|
|
||||||
|
|
||||||
[[song]]
|
|
||||||
name = "Stairway to Heaven"
|
|
||||||
duration = "8m03s"
|
|
||||||
```
|
|
||||||
|
|
||||||
Which can be decoded with:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type song struct {
|
|
||||||
Name string
|
|
||||||
Duration duration
|
|
||||||
}
|
|
||||||
type songs struct {
|
|
||||||
Song []song
|
|
||||||
}
|
|
||||||
var favorites songs
|
|
||||||
if _, err := toml.Decode(blob, &favorites); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, s := range favorites.Song {
|
|
||||||
fmt.Printf("%s (%s)\n", s.Name, s.Duration)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
And you'll also need a `duration` type that satisfies the
|
|
||||||
`encoding.TextUnmarshaler` interface:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type duration struct {
|
|
||||||
time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *duration) UnmarshalText(text []byte) error {
|
|
||||||
var err error
|
|
||||||
d.Duration, err = time.ParseDuration(string(text))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### More complex usage
|
|
||||||
|
|
||||||
Here's an example of how to load the example from the official spec page:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
# This is a TOML document. Boom.
|
|
||||||
|
|
||||||
title = "TOML Example"
|
|
||||||
|
|
||||||
[owner]
|
|
||||||
name = "Tom Preston-Werner"
|
|
||||||
organization = "GitHub"
|
|
||||||
bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
|
|
||||||
dob = 1979-05-27T07:32:00Z # First class dates? Why not?
|
|
||||||
|
|
||||||
[database]
|
|
||||||
server = "192.168.1.1"
|
|
||||||
ports = [ 8001, 8001, 8002 ]
|
|
||||||
connection_max = 5000
|
|
||||||
enabled = true
|
|
||||||
|
|
||||||
[servers]
|
|
||||||
|
|
||||||
# You can indent as you please. Tabs or spaces. TOML don't care.
|
|
||||||
[servers.alpha]
|
|
||||||
ip = "10.0.0.1"
|
|
||||||
dc = "eqdc10"
|
|
||||||
|
|
||||||
[servers.beta]
|
|
||||||
ip = "10.0.0.2"
|
|
||||||
dc = "eqdc10"
|
|
||||||
|
|
||||||
[clients]
|
|
||||||
data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it
|
|
||||||
|
|
||||||
# Line breaks are OK when inside arrays
|
|
||||||
hosts = [
|
|
||||||
"alpha",
|
|
||||||
"omega"
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
And the corresponding Go types are:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type tomlConfig struct {
|
|
||||||
Title string
|
|
||||||
Owner ownerInfo
|
|
||||||
DB database `toml:"database"`
|
|
||||||
Servers map[string]server
|
|
||||||
Clients clients
|
|
||||||
}
|
|
||||||
|
|
||||||
type ownerInfo struct {
|
|
||||||
Name string
|
|
||||||
Org string `toml:"organization"`
|
|
||||||
Bio string
|
|
||||||
DOB time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type database struct {
|
|
||||||
Server string
|
|
||||||
Ports []int
|
|
||||||
ConnMax int `toml:"connection_max"`
|
|
||||||
Enabled bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type server struct {
|
|
||||||
IP string
|
|
||||||
DC string
|
|
||||||
}
|
|
||||||
|
|
||||||
type clients struct {
|
|
||||||
Data [][]interface{}
|
|
||||||
Hosts []string
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that a case insensitive match will be tried if an exact match can't be
|
|
||||||
found.
|
|
||||||
|
|
||||||
A working example of the above can be found in `_examples/example.{go,toml}`.
|
|
||||||
61
vendor/github.com/BurntSushi/toml/_examples/example.go
generated
vendored
61
vendor/github.com/BurntSushi/toml/_examples/example.go
generated
vendored
@ -1,61 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/BurntSushi/toml"
|
|
||||||
)
|
|
||||||
|
|
||||||
type tomlConfig struct {
|
|
||||||
Title string
|
|
||||||
Owner ownerInfo
|
|
||||||
DB database `toml:"database"`
|
|
||||||
Servers map[string]server
|
|
||||||
Clients clients
|
|
||||||
}
|
|
||||||
|
|
||||||
type ownerInfo struct {
|
|
||||||
Name string
|
|
||||||
Org string `toml:"organization"`
|
|
||||||
Bio string
|
|
||||||
DOB time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type database struct {
|
|
||||||
Server string
|
|
||||||
Ports []int
|
|
||||||
ConnMax int `toml:"connection_max"`
|
|
||||||
Enabled bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type server struct {
|
|
||||||
IP string
|
|
||||||
DC string
|
|
||||||
}
|
|
||||||
|
|
||||||
type clients struct {
|
|
||||||
Data [][]interface{}
|
|
||||||
Hosts []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
var config tomlConfig
|
|
||||||
if _, err := toml.DecodeFile("example.toml", &config); err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Title: %s\n", config.Title)
|
|
||||||
fmt.Printf("Owner: %s (%s, %s), Born: %s\n",
|
|
||||||
config.Owner.Name, config.Owner.Org, config.Owner.Bio,
|
|
||||||
config.Owner.DOB)
|
|
||||||
fmt.Printf("Database: %s %v (Max conn. %d), Enabled? %v\n",
|
|
||||||
config.DB.Server, config.DB.Ports, config.DB.ConnMax,
|
|
||||||
config.DB.Enabled)
|
|
||||||
for serverName, server := range config.Servers {
|
|
||||||
fmt.Printf("Server: %s (%s, %s)\n", serverName, server.IP, server.DC)
|
|
||||||
}
|
|
||||||
fmt.Printf("Client data: %v\n", config.Clients.Data)
|
|
||||||
fmt.Printf("Client hosts: %v\n", config.Clients.Hosts)
|
|
||||||
}
|
|
||||||
35
vendor/github.com/BurntSushi/toml/_examples/example.toml
generated
vendored
35
vendor/github.com/BurntSushi/toml/_examples/example.toml
generated
vendored
@ -1,35 +0,0 @@
|
|||||||
# This is a TOML document. Boom.
|
|
||||||
|
|
||||||
title = "TOML Example"
|
|
||||||
|
|
||||||
[owner]
|
|
||||||
name = "Tom Preston-Werner"
|
|
||||||
organization = "GitHub"
|
|
||||||
bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
|
|
||||||
dob = 1979-05-27T07:32:00Z # First class dates? Why not?
|
|
||||||
|
|
||||||
[database]
|
|
||||||
server = "192.168.1.1"
|
|
||||||
ports = [ 8001, 8001, 8002 ]
|
|
||||||
connection_max = 5000
|
|
||||||
enabled = true
|
|
||||||
|
|
||||||
[servers]
|
|
||||||
|
|
||||||
# You can indent as you please. Tabs or spaces. TOML don't care.
|
|
||||||
[servers.alpha]
|
|
||||||
ip = "10.0.0.1"
|
|
||||||
dc = "eqdc10"
|
|
||||||
|
|
||||||
[servers.beta]
|
|
||||||
ip = "10.0.0.2"
|
|
||||||
dc = "eqdc10"
|
|
||||||
|
|
||||||
[clients]
|
|
||||||
data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it
|
|
||||||
|
|
||||||
# Line breaks are OK when inside arrays
|
|
||||||
hosts = [
|
|
||||||
"alpha",
|
|
||||||
"omega"
|
|
||||||
]
|
|
||||||
22
vendor/github.com/BurntSushi/toml/_examples/hard.toml
generated
vendored
22
vendor/github.com/BurntSushi/toml/_examples/hard.toml
generated
vendored
@ -1,22 +0,0 @@
|
|||||||
# Test file for TOML
|
|
||||||
# Only this one tries to emulate a TOML file written by a user of the kind of parser writers probably hate
|
|
||||||
# This part you'll really hate
|
|
||||||
|
|
||||||
[the]
|
|
||||||
test_string = "You'll hate me after this - #" # " Annoying, isn't it?
|
|
||||||
|
|
||||||
[the.hard]
|
|
||||||
test_array = [ "] ", " # "] # ] There you go, parse this!
|
|
||||||
test_array2 = [ "Test #11 ]proved that", "Experiment #9 was a success" ]
|
|
||||||
# You didn't think it'd as easy as chucking out the last #, did you?
|
|
||||||
another_test_string = " Same thing, but with a string #"
|
|
||||||
harder_test_string = " And when \"'s are in the string, along with # \"" # "and comments are there too"
|
|
||||||
# Things will get harder
|
|
||||||
|
|
||||||
[the.hard.bit#]
|
|
||||||
what? = "You don't think some user won't do that?"
|
|
||||||
multi_line_array = [
|
|
||||||
"]",
|
|
||||||
# ] Oh yes I did
|
|
||||||
]
|
|
||||||
|
|
||||||
4
vendor/github.com/BurntSushi/toml/_examples/implicit.toml
generated
vendored
4
vendor/github.com/BurntSushi/toml/_examples/implicit.toml
generated
vendored
@ -1,4 +0,0 @@
|
|||||||
# [x] you
|
|
||||||
# [x.y] don't
|
|
||||||
# [x.y.z] need these
|
|
||||||
[x.y.z.w] # for this to work
|
|
||||||
6
vendor/github.com/BurntSushi/toml/_examples/invalid-apples.toml
generated
vendored
6
vendor/github.com/BurntSushi/toml/_examples/invalid-apples.toml
generated
vendored
@ -1,6 +0,0 @@
|
|||||||
# DO NOT WANT
|
|
||||||
[fruit]
|
|
||||||
type = "apple"
|
|
||||||
|
|
||||||
[fruit.type]
|
|
||||||
apple = "yes"
|
|
||||||
35
vendor/github.com/BurntSushi/toml/_examples/invalid.toml
generated
vendored
35
vendor/github.com/BurntSushi/toml/_examples/invalid.toml
generated
vendored
@ -1,35 +0,0 @@
|
|||||||
# This is an INVALID TOML document. Boom.
|
|
||||||
# Can you spot the error without help?
|
|
||||||
|
|
||||||
title = "TOML Example"
|
|
||||||
|
|
||||||
[owner]
|
|
||||||
name = "Tom Preston-Werner"
|
|
||||||
organization = "GitHub"
|
|
||||||
bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
|
|
||||||
dob = 1979-05-27T7:32:00Z # First class dates? Why not?
|
|
||||||
|
|
||||||
[database]
|
|
||||||
server = "192.168.1.1"
|
|
||||||
ports = [ 8001, 8001, 8002 ]
|
|
||||||
connection_max = 5000
|
|
||||||
enabled = true
|
|
||||||
|
|
||||||
[servers]
|
|
||||||
# You can indent as you please. Tabs or spaces. TOML don't care.
|
|
||||||
[servers.alpha]
|
|
||||||
ip = "10.0.0.1"
|
|
||||||
dc = "eqdc10"
|
|
||||||
|
|
||||||
[servers.beta]
|
|
||||||
ip = "10.0.0.2"
|
|
||||||
dc = "eqdc10"
|
|
||||||
|
|
||||||
[clients]
|
|
||||||
data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it
|
|
||||||
|
|
||||||
# Line breaks are OK when inside arrays
|
|
||||||
hosts = [
|
|
||||||
"alpha",
|
|
||||||
"omega"
|
|
||||||
]
|
|
||||||
5
vendor/github.com/BurntSushi/toml/_examples/readme1.toml
generated
vendored
5
vendor/github.com/BurntSushi/toml/_examples/readme1.toml
generated
vendored
@ -1,5 +0,0 @@
|
|||||||
Age = 25
|
|
||||||
Cats = [ "Cauchy", "Plato" ]
|
|
||||||
Pi = 3.14
|
|
||||||
Perfection = [ 6, 28, 496, 8128 ]
|
|
||||||
DOB = 1987-07-05T05:45:00Z
|
|
||||||
1
vendor/github.com/BurntSushi/toml/_examples/readme2.toml
generated
vendored
1
vendor/github.com/BurntSushi/toml/_examples/readme2.toml
generated
vendored
@ -1 +0,0 @@
|
|||||||
some_key_NAME = "wat"
|
|
||||||
14
vendor/github.com/BurntSushi/toml/cmd/toml-test-decoder/COPYING
generated
vendored
14
vendor/github.com/BurntSushi/toml/cmd/toml-test-decoder/COPYING
generated
vendored
@ -1,14 +0,0 @@
|
|||||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
||||||
Version 2, December 2004
|
|
||||||
|
|
||||||
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
|
||||||
|
|
||||||
Everyone is permitted to copy and distribute verbatim or modified
|
|
||||||
copies of this license document, and changing it is allowed as long
|
|
||||||
as the name is changed.
|
|
||||||
|
|
||||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
||||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
|
||||||
|
|
||||||
0. You just DO WHAT THE FUCK YOU WANT TO.
|
|
||||||
|
|
||||||
13
vendor/github.com/BurntSushi/toml/cmd/toml-test-decoder/README.md
generated
vendored
13
vendor/github.com/BurntSushi/toml/cmd/toml-test-decoder/README.md
generated
vendored
@ -1,13 +0,0 @@
|
|||||||
# Implements the TOML test suite interface
|
|
||||||
|
|
||||||
This is an implementation of the interface expected by
|
|
||||||
[toml-test](https://github.com/BurntSushi/toml-test) for my
|
|
||||||
[toml parser written in Go](https://github.com/BurntSushi/toml).
|
|
||||||
In particular, it maps TOML data on `stdin` to a JSON format on `stdout`.
|
|
||||||
|
|
||||||
|
|
||||||
Compatible with TOML version
|
|
||||||
[v0.4.0](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md)
|
|
||||||
|
|
||||||
Compatible with `toml-test` version
|
|
||||||
[v0.2.0](https://github.com/BurntSushi/toml-test/tree/v0.2.0)
|
|
||||||
90
vendor/github.com/BurntSushi/toml/cmd/toml-test-decoder/main.go
generated
vendored
90
vendor/github.com/BurntSushi/toml/cmd/toml-test-decoder/main.go
generated
vendored
@ -1,90 +0,0 @@
|
|||||||
// Command toml-test-decoder satisfies the toml-test interface for testing
|
|
||||||
// TOML decoders. Namely, it accepts TOML on stdin and outputs JSON on stdout.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/BurntSushi/toml"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
log.SetFlags(0)
|
|
||||||
|
|
||||||
flag.Usage = usage
|
|
||||||
flag.Parse()
|
|
||||||
}
|
|
||||||
|
|
||||||
func usage() {
|
|
||||||
log.Printf("Usage: %s < toml-file\n", path.Base(os.Args[0]))
|
|
||||||
flag.PrintDefaults()
|
|
||||||
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
if flag.NArg() != 0 {
|
|
||||||
flag.Usage()
|
|
||||||
}
|
|
||||||
|
|
||||||
var tmp interface{}
|
|
||||||
if _, err := toml.DecodeReader(os.Stdin, &tmp); err != nil {
|
|
||||||
log.Fatalf("Error decoding TOML: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
typedTmp := translate(tmp)
|
|
||||||
if err := json.NewEncoder(os.Stdout).Encode(typedTmp); err != nil {
|
|
||||||
log.Fatalf("Error encoding JSON: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func translate(tomlData interface{}) interface{} {
|
|
||||||
switch orig := tomlData.(type) {
|
|
||||||
case map[string]interface{}:
|
|
||||||
typed := make(map[string]interface{}, len(orig))
|
|
||||||
for k, v := range orig {
|
|
||||||
typed[k] = translate(v)
|
|
||||||
}
|
|
||||||
return typed
|
|
||||||
case []map[string]interface{}:
|
|
||||||
typed := make([]map[string]interface{}, len(orig))
|
|
||||||
for i, v := range orig {
|
|
||||||
typed[i] = translate(v).(map[string]interface{})
|
|
||||||
}
|
|
||||||
return typed
|
|
||||||
case []interface{}:
|
|
||||||
typed := make([]interface{}, len(orig))
|
|
||||||
for i, v := range orig {
|
|
||||||
typed[i] = translate(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We don't really need to tag arrays, but let's be future proof.
|
|
||||||
// (If TOML ever supports tuples, we'll need this.)
|
|
||||||
return tag("array", typed)
|
|
||||||
case time.Time:
|
|
||||||
return tag("datetime", orig.Format("2006-01-02T15:04:05Z"))
|
|
||||||
case bool:
|
|
||||||
return tag("bool", fmt.Sprintf("%v", orig))
|
|
||||||
case int64:
|
|
||||||
return tag("integer", fmt.Sprintf("%d", orig))
|
|
||||||
case float64:
|
|
||||||
return tag("float", fmt.Sprintf("%v", orig))
|
|
||||||
case string:
|
|
||||||
return tag("string", orig)
|
|
||||||
}
|
|
||||||
|
|
||||||
panic(fmt.Sprintf("Unknown type: %T", tomlData))
|
|
||||||
}
|
|
||||||
|
|
||||||
func tag(typeName string, data interface{}) map[string]interface{} {
|
|
||||||
return map[string]interface{}{
|
|
||||||
"type": typeName,
|
|
||||||
"value": data,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
14
vendor/github.com/BurntSushi/toml/cmd/toml-test-encoder/COPYING
generated
vendored
14
vendor/github.com/BurntSushi/toml/cmd/toml-test-encoder/COPYING
generated
vendored
@ -1,14 +0,0 @@
|
|||||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
||||||
Version 2, December 2004
|
|
||||||
|
|
||||||
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
|
||||||
|
|
||||||
Everyone is permitted to copy and distribute verbatim or modified
|
|
||||||
copies of this license document, and changing it is allowed as long
|
|
||||||
as the name is changed.
|
|
||||||
|
|
||||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
||||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
|
||||||
|
|
||||||
0. You just DO WHAT THE FUCK YOU WANT TO.
|
|
||||||
|
|
||||||
13
vendor/github.com/BurntSushi/toml/cmd/toml-test-encoder/README.md
generated
vendored
13
vendor/github.com/BurntSushi/toml/cmd/toml-test-encoder/README.md
generated
vendored
@ -1,13 +0,0 @@
|
|||||||
# Implements the TOML test suite interface for TOML encoders
|
|
||||||
|
|
||||||
This is an implementation of the interface expected by
|
|
||||||
[toml-test](https://github.com/BurntSushi/toml-test) for the
|
|
||||||
[TOML encoder](https://github.com/BurntSushi/toml).
|
|
||||||
In particular, it maps JSON data on `stdin` to a TOML format on `stdout`.
|
|
||||||
|
|
||||||
|
|
||||||
Compatible with TOML version
|
|
||||||
[v0.4.0](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md)
|
|
||||||
|
|
||||||
Compatible with `toml-test` version
|
|
||||||
[v0.2.0](https://github.com/BurntSushi/toml-test/tree/v0.2.0)
|
|
||||||
131
vendor/github.com/BurntSushi/toml/cmd/toml-test-encoder/main.go
generated
vendored
131
vendor/github.com/BurntSushi/toml/cmd/toml-test-encoder/main.go
generated
vendored
@ -1,131 +0,0 @@
|
|||||||
// Command toml-test-encoder satisfies the toml-test interface for testing
|
|
||||||
// TOML encoders. Namely, it accepts JSON on stdin and outputs TOML on stdout.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/BurntSushi/toml"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
log.SetFlags(0)
|
|
||||||
|
|
||||||
flag.Usage = usage
|
|
||||||
flag.Parse()
|
|
||||||
}
|
|
||||||
|
|
||||||
func usage() {
|
|
||||||
log.Printf("Usage: %s < json-file\n", path.Base(os.Args[0]))
|
|
||||||
flag.PrintDefaults()
|
|
||||||
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
if flag.NArg() != 0 {
|
|
||||||
flag.Usage()
|
|
||||||
}
|
|
||||||
|
|
||||||
var tmp interface{}
|
|
||||||
if err := json.NewDecoder(os.Stdin).Decode(&tmp); err != nil {
|
|
||||||
log.Fatalf("Error decoding JSON: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tomlData := translate(tmp)
|
|
||||||
if err := toml.NewEncoder(os.Stdout).Encode(tomlData); err != nil {
|
|
||||||
log.Fatalf("Error encoding TOML: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func translate(typedJson interface{}) interface{} {
|
|
||||||
switch v := typedJson.(type) {
|
|
||||||
case map[string]interface{}:
|
|
||||||
if len(v) == 2 && in("type", v) && in("value", v) {
|
|
||||||
return untag(v)
|
|
||||||
}
|
|
||||||
m := make(map[string]interface{}, len(v))
|
|
||||||
for k, v2 := range v {
|
|
||||||
m[k] = translate(v2)
|
|
||||||
}
|
|
||||||
return m
|
|
||||||
case []interface{}:
|
|
||||||
tabArray := make([]map[string]interface{}, len(v))
|
|
||||||
for i := range v {
|
|
||||||
if m, ok := translate(v[i]).(map[string]interface{}); ok {
|
|
||||||
tabArray[i] = m
|
|
||||||
} else {
|
|
||||||
log.Fatalf("JSON arrays may only contain objects. This " +
|
|
||||||
"corresponds to only tables being allowed in " +
|
|
||||||
"TOML table arrays.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tabArray
|
|
||||||
}
|
|
||||||
log.Fatalf("Unrecognized JSON format '%T'.", typedJson)
|
|
||||||
panic("unreachable")
|
|
||||||
}
|
|
||||||
|
|
||||||
func untag(typed map[string]interface{}) interface{} {
|
|
||||||
t := typed["type"].(string)
|
|
||||||
v := typed["value"]
|
|
||||||
switch t {
|
|
||||||
case "string":
|
|
||||||
return v.(string)
|
|
||||||
case "integer":
|
|
||||||
v := v.(string)
|
|
||||||
n, err := strconv.Atoi(v)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Could not parse '%s' as integer: %s", v, err)
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
case "float":
|
|
||||||
v := v.(string)
|
|
||||||
f, err := strconv.ParseFloat(v, 64)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Could not parse '%s' as float64: %s", v, err)
|
|
||||||
}
|
|
||||||
return f
|
|
||||||
case "datetime":
|
|
||||||
v := v.(string)
|
|
||||||
t, err := time.Parse("2006-01-02T15:04:05Z", v)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Could not parse '%s' as a datetime: %s", v, err)
|
|
||||||
}
|
|
||||||
return t
|
|
||||||
case "bool":
|
|
||||||
v := v.(string)
|
|
||||||
switch v {
|
|
||||||
case "true":
|
|
||||||
return true
|
|
||||||
case "false":
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
log.Fatalf("Could not parse '%s' as a boolean.", v)
|
|
||||||
case "array":
|
|
||||||
v := v.([]interface{})
|
|
||||||
array := make([]interface{}, len(v))
|
|
||||||
for i := range v {
|
|
||||||
if m, ok := v[i].(map[string]interface{}); ok {
|
|
||||||
array[i] = untag(m)
|
|
||||||
} else {
|
|
||||||
log.Fatalf("Arrays may only contain other arrays or "+
|
|
||||||
"primitive values, but found a '%T'.", m)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return array
|
|
||||||
}
|
|
||||||
log.Fatalf("Unrecognized tag type '%s'.", t)
|
|
||||||
panic("unreachable")
|
|
||||||
}
|
|
||||||
|
|
||||||
func in(key string, m map[string]interface{}) bool {
|
|
||||||
_, ok := m[key]
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
14
vendor/github.com/BurntSushi/toml/cmd/tomlv/COPYING
generated
vendored
14
vendor/github.com/BurntSushi/toml/cmd/tomlv/COPYING
generated
vendored
@ -1,14 +0,0 @@
|
|||||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
||||||
Version 2, December 2004
|
|
||||||
|
|
||||||
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
|
||||||
|
|
||||||
Everyone is permitted to copy and distribute verbatim or modified
|
|
||||||
copies of this license document, and changing it is allowed as long
|
|
||||||
as the name is changed.
|
|
||||||
|
|
||||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
||||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
|
||||||
|
|
||||||
0. You just DO WHAT THE FUCK YOU WANT TO.
|
|
||||||
|
|
||||||
21
vendor/github.com/BurntSushi/toml/cmd/tomlv/README.md
generated
vendored
21
vendor/github.com/BurntSushi/toml/cmd/tomlv/README.md
generated
vendored
@ -1,21 +0,0 @@
|
|||||||
# TOML Validator
|
|
||||||
|
|
||||||
If Go is installed, it's simple to try it out:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go get github.com/BurntSushi/toml/cmd/tomlv
|
|
||||||
tomlv some-toml-file.toml
|
|
||||||
```
|
|
||||||
|
|
||||||
You can see the types of every key in a TOML file with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
tomlv -types some-toml-file.toml
|
|
||||||
```
|
|
||||||
|
|
||||||
At the moment, only one error message is reported at a time. Error messages
|
|
||||||
include line numbers. No output means that the files given are valid TOML, or
|
|
||||||
there is a bug in `tomlv`.
|
|
||||||
|
|
||||||
Compatible with TOML version
|
|
||||||
[v0.4.0](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md)
|
|
||||||
61
vendor/github.com/BurntSushi/toml/cmd/tomlv/main.go
generated
vendored
61
vendor/github.com/BurntSushi/toml/cmd/tomlv/main.go
generated
vendored
@ -1,61 +0,0 @@
|
|||||||
// Command tomlv validates TOML documents and prints each key's type.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
"text/tabwriter"
|
|
||||||
|
|
||||||
"github.com/BurntSushi/toml"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
flagTypes = false
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
log.SetFlags(0)
|
|
||||||
|
|
||||||
flag.BoolVar(&flagTypes, "types", flagTypes,
|
|
||||||
"When set, the types of every defined key will be shown.")
|
|
||||||
|
|
||||||
flag.Usage = usage
|
|
||||||
flag.Parse()
|
|
||||||
}
|
|
||||||
|
|
||||||
func usage() {
|
|
||||||
log.Printf("Usage: %s toml-file [ toml-file ... ]\n",
|
|
||||||
path.Base(os.Args[0]))
|
|
||||||
flag.PrintDefaults()
|
|
||||||
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
if flag.NArg() < 1 {
|
|
||||||
flag.Usage()
|
|
||||||
}
|
|
||||||
for _, f := range flag.Args() {
|
|
||||||
var tmp interface{}
|
|
||||||
md, err := toml.DecodeFile(f, &tmp)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error in '%s': %s", f, err)
|
|
||||||
}
|
|
||||||
if flagTypes {
|
|
||||||
printTypes(md)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func printTypes(md toml.MetaData) {
|
|
||||||
tabw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
|
||||||
for _, key := range md.Keys() {
|
|
||||||
fmt.Fprintf(tabw, "%s%s\t%s\n",
|
|
||||||
strings.Repeat(" ", len(key)-1), key, md.Type(key...))
|
|
||||||
}
|
|
||||||
tabw.Flush()
|
|
||||||
}
|
|
||||||
509
vendor/github.com/BurntSushi/toml/decode.go
generated
vendored
509
vendor/github.com/BurntSushi/toml/decode.go
generated
vendored
@ -1,509 +0,0 @@
|
|||||||
package toml
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"math"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func e(format string, args ...interface{}) error {
|
|
||||||
return fmt.Errorf("toml: "+format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshaler is the interface implemented by objects that can unmarshal a
|
|
||||||
// TOML description of themselves.
|
|
||||||
type Unmarshaler interface {
|
|
||||||
UnmarshalTOML(interface{}) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal decodes the contents of `p` in TOML format into a pointer `v`.
|
|
||||||
func Unmarshal(p []byte, v interface{}) error {
|
|
||||||
_, err := Decode(string(p), v)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Primitive is a TOML value that hasn't been decoded into a Go value.
|
|
||||||
// When using the various `Decode*` functions, the type `Primitive` may
|
|
||||||
// be given to any value, and its decoding will be delayed.
|
|
||||||
//
|
|
||||||
// A `Primitive` value can be decoded using the `PrimitiveDecode` function.
|
|
||||||
//
|
|
||||||
// The underlying representation of a `Primitive` value is subject to change.
|
|
||||||
// Do not rely on it.
|
|
||||||
//
|
|
||||||
// N.B. Primitive values are still parsed, so using them will only avoid
|
|
||||||
// the overhead of reflection. They can be useful when you don't know the
|
|
||||||
// exact type of TOML data until run time.
|
|
||||||
type Primitive struct {
|
|
||||||
undecoded interface{}
|
|
||||||
context Key
|
|
||||||
}
|
|
||||||
|
|
||||||
// DEPRECATED!
|
|
||||||
//
|
|
||||||
// Use MetaData.PrimitiveDecode instead.
|
|
||||||
func PrimitiveDecode(primValue Primitive, v interface{}) error {
|
|
||||||
md := MetaData{decoded: make(map[string]bool)}
|
|
||||||
return md.unify(primValue.undecoded, rvalue(v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrimitiveDecode is just like the other `Decode*` functions, except it
|
|
||||||
// decodes a TOML value that has already been parsed. Valid primitive values
|
|
||||||
// can *only* be obtained from values filled by the decoder functions,
|
|
||||||
// including this method. (i.e., `v` may contain more `Primitive`
|
|
||||||
// values.)
|
|
||||||
//
|
|
||||||
// Meta data for primitive values is included in the meta data returned by
|
|
||||||
// the `Decode*` functions with one exception: keys returned by the Undecoded
|
|
||||||
// method will only reflect keys that were decoded. Namely, any keys hidden
|
|
||||||
// behind a Primitive will be considered undecoded. Executing this method will
|
|
||||||
// update the undecoded keys in the meta data. (See the example.)
|
|
||||||
func (md *MetaData) PrimitiveDecode(primValue Primitive, v interface{}) error {
|
|
||||||
md.context = primValue.context
|
|
||||||
defer func() { md.context = nil }()
|
|
||||||
return md.unify(primValue.undecoded, rvalue(v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode will decode the contents of `data` in TOML format into a pointer
|
|
||||||
// `v`.
|
|
||||||
//
|
|
||||||
// TOML hashes correspond to Go structs or maps. (Dealer's choice. They can be
|
|
||||||
// used interchangeably.)
|
|
||||||
//
|
|
||||||
// TOML arrays of tables correspond to either a slice of structs or a slice
|
|
||||||
// of maps.
|
|
||||||
//
|
|
||||||
// TOML datetimes correspond to Go `time.Time` values.
|
|
||||||
//
|
|
||||||
// All other TOML types (float, string, int, bool and array) correspond
|
|
||||||
// to the obvious Go types.
|
|
||||||
//
|
|
||||||
// An exception to the above rules is if a type implements the
|
|
||||||
// encoding.TextUnmarshaler interface. In this case, any primitive TOML value
|
|
||||||
// (floats, strings, integers, booleans and datetimes) will be converted to
|
|
||||||
// a byte string and given to the value's UnmarshalText method. See the
|
|
||||||
// Unmarshaler example for a demonstration with time duration strings.
|
|
||||||
//
|
|
||||||
// Key mapping
|
|
||||||
//
|
|
||||||
// TOML keys can map to either keys in a Go map or field names in a Go
|
|
||||||
// struct. The special `toml` struct tag may be used to map TOML keys to
|
|
||||||
// struct fields that don't match the key name exactly. (See the example.)
|
|
||||||
// A case insensitive match to struct names will be tried if an exact match
|
|
||||||
// can't be found.
|
|
||||||
//
|
|
||||||
// The mapping between TOML values and Go values is loose. That is, there
|
|
||||||
// may exist TOML values that cannot be placed into your representation, and
|
|
||||||
// there may be parts of your representation that do not correspond to
|
|
||||||
// TOML values. This loose mapping can be made stricter by using the IsDefined
|
|
||||||
// and/or Undecoded methods on the MetaData returned.
|
|
||||||
//
|
|
||||||
// This decoder will not handle cyclic types. If a cyclic type is passed,
|
|
||||||
// `Decode` will not terminate.
|
|
||||||
func Decode(data string, v interface{}) (MetaData, error) {
|
|
||||||
rv := reflect.ValueOf(v)
|
|
||||||
if rv.Kind() != reflect.Ptr {
|
|
||||||
return MetaData{}, e("Decode of non-pointer %s", reflect.TypeOf(v))
|
|
||||||
}
|
|
||||||
if rv.IsNil() {
|
|
||||||
return MetaData{}, e("Decode of nil %s", reflect.TypeOf(v))
|
|
||||||
}
|
|
||||||
p, err := parse(data)
|
|
||||||
if err != nil {
|
|
||||||
return MetaData{}, err
|
|
||||||
}
|
|
||||||
md := MetaData{
|
|
||||||
p.mapping, p.types, p.ordered,
|
|
||||||
make(map[string]bool, len(p.ordered)), nil,
|
|
||||||
}
|
|
||||||
return md, md.unify(p.mapping, indirect(rv))
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecodeFile is just like Decode, except it will automatically read the
|
|
||||||
// contents of the file at `fpath` and decode it for you.
|
|
||||||
func DecodeFile(fpath string, v interface{}) (MetaData, error) {
|
|
||||||
bs, err := ioutil.ReadFile(fpath)
|
|
||||||
if err != nil {
|
|
||||||
return MetaData{}, err
|
|
||||||
}
|
|
||||||
return Decode(string(bs), v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecodeReader is just like Decode, except it will consume all bytes
|
|
||||||
// from the reader and decode it for you.
|
|
||||||
func DecodeReader(r io.Reader, v interface{}) (MetaData, error) {
|
|
||||||
bs, err := ioutil.ReadAll(r)
|
|
||||||
if err != nil {
|
|
||||||
return MetaData{}, err
|
|
||||||
}
|
|
||||||
return Decode(string(bs), v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// unify performs a sort of type unification based on the structure of `rv`,
|
|
||||||
// which is the client representation.
|
|
||||||
//
|
|
||||||
// Any type mismatch produces an error. Finding a type that we don't know
|
|
||||||
// how to handle produces an unsupported type error.
|
|
||||||
func (md *MetaData) unify(data interface{}, rv reflect.Value) error {
|
|
||||||
|
|
||||||
// Special case. Look for a `Primitive` value.
|
|
||||||
if rv.Type() == reflect.TypeOf((*Primitive)(nil)).Elem() {
|
|
||||||
// Save the undecoded data and the key context into the primitive
|
|
||||||
// value.
|
|
||||||
context := make(Key, len(md.context))
|
|
||||||
copy(context, md.context)
|
|
||||||
rv.Set(reflect.ValueOf(Primitive{
|
|
||||||
undecoded: data,
|
|
||||||
context: context,
|
|
||||||
}))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special case. Unmarshaler Interface support.
|
|
||||||
if rv.CanAddr() {
|
|
||||||
if v, ok := rv.Addr().Interface().(Unmarshaler); ok {
|
|
||||||
return v.UnmarshalTOML(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special case. Handle time.Time values specifically.
|
|
||||||
// TODO: Remove this code when we decide to drop support for Go 1.1.
|
|
||||||
// This isn't necessary in Go 1.2 because time.Time satisfies the encoding
|
|
||||||
// interfaces.
|
|
||||||
if rv.Type().AssignableTo(rvalue(time.Time{}).Type()) {
|
|
||||||
return md.unifyDatetime(data, rv)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special case. Look for a value satisfying the TextUnmarshaler interface.
|
|
||||||
if v, ok := rv.Interface().(TextUnmarshaler); ok {
|
|
||||||
return md.unifyText(data, v)
|
|
||||||
}
|
|
||||||
// BUG(burntsushi)
|
|
||||||
// The behavior here is incorrect whenever a Go type satisfies the
|
|
||||||
// encoding.TextUnmarshaler interface but also corresponds to a TOML
|
|
||||||
// hash or array. In particular, the unmarshaler should only be applied
|
|
||||||
// to primitive TOML values. But at this point, it will be applied to
|
|
||||||
// all kinds of values and produce an incorrect error whenever those values
|
|
||||||
// are hashes or arrays (including arrays of tables).
|
|
||||||
|
|
||||||
k := rv.Kind()
|
|
||||||
|
|
||||||
// laziness
|
|
||||||
if k >= reflect.Int && k <= reflect.Uint64 {
|
|
||||||
return md.unifyInt(data, rv)
|
|
||||||
}
|
|
||||||
switch k {
|
|
||||||
case reflect.Ptr:
|
|
||||||
elem := reflect.New(rv.Type().Elem())
|
|
||||||
err := md.unify(data, reflect.Indirect(elem))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
rv.Set(elem)
|
|
||||||
return nil
|
|
||||||
case reflect.Struct:
|
|
||||||
return md.unifyStruct(data, rv)
|
|
||||||
case reflect.Map:
|
|
||||||
return md.unifyMap(data, rv)
|
|
||||||
case reflect.Array:
|
|
||||||
return md.unifyArray(data, rv)
|
|
||||||
case reflect.Slice:
|
|
||||||
return md.unifySlice(data, rv)
|
|
||||||
case reflect.String:
|
|
||||||
return md.unifyString(data, rv)
|
|
||||||
case reflect.Bool:
|
|
||||||
return md.unifyBool(data, rv)
|
|
||||||
case reflect.Interface:
|
|
||||||
// we only support empty interfaces.
|
|
||||||
if rv.NumMethod() > 0 {
|
|
||||||
return e("unsupported type %s", rv.Type())
|
|
||||||
}
|
|
||||||
return md.unifyAnything(data, rv)
|
|
||||||
case reflect.Float32:
|
|
||||||
fallthrough
|
|
||||||
case reflect.Float64:
|
|
||||||
return md.unifyFloat64(data, rv)
|
|
||||||
}
|
|
||||||
return e("unsupported type %s", rv.Kind())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error {
|
|
||||||
tmap, ok := mapping.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
if mapping == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return e("type mismatch for %s: expected table but found %T",
|
|
||||||
rv.Type().String(), mapping)
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, datum := range tmap {
|
|
||||||
var f *field
|
|
||||||
fields := cachedTypeFields(rv.Type())
|
|
||||||
for i := range fields {
|
|
||||||
ff := &fields[i]
|
|
||||||
if ff.name == key {
|
|
||||||
f = ff
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if f == nil && strings.EqualFold(ff.name, key) {
|
|
||||||
f = ff
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if f != nil {
|
|
||||||
subv := rv
|
|
||||||
for _, i := range f.index {
|
|
||||||
subv = indirect(subv.Field(i))
|
|
||||||
}
|
|
||||||
if isUnifiable(subv) {
|
|
||||||
md.decoded[md.context.add(key).String()] = true
|
|
||||||
md.context = append(md.context, key)
|
|
||||||
if err := md.unify(datum, subv); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
md.context = md.context[0 : len(md.context)-1]
|
|
||||||
} else if f.name != "" {
|
|
||||||
// Bad user! No soup for you!
|
|
||||||
return e("cannot write unexported field %s.%s",
|
|
||||||
rv.Type().String(), f.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (md *MetaData) unifyMap(mapping interface{}, rv reflect.Value) error {
|
|
||||||
tmap, ok := mapping.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
if tmap == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return badtype("map", mapping)
|
|
||||||
}
|
|
||||||
if rv.IsNil() {
|
|
||||||
rv.Set(reflect.MakeMap(rv.Type()))
|
|
||||||
}
|
|
||||||
for k, v := range tmap {
|
|
||||||
md.decoded[md.context.add(k).String()] = true
|
|
||||||
md.context = append(md.context, k)
|
|
||||||
|
|
||||||
rvkey := indirect(reflect.New(rv.Type().Key()))
|
|
||||||
rvval := reflect.Indirect(reflect.New(rv.Type().Elem()))
|
|
||||||
if err := md.unify(v, rvval); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
md.context = md.context[0 : len(md.context)-1]
|
|
||||||
|
|
||||||
rvkey.SetString(k)
|
|
||||||
rv.SetMapIndex(rvkey, rvval)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (md *MetaData) unifyArray(data interface{}, rv reflect.Value) error {
|
|
||||||
datav := reflect.ValueOf(data)
|
|
||||||
if datav.Kind() != reflect.Slice {
|
|
||||||
if !datav.IsValid() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return badtype("slice", data)
|
|
||||||
}
|
|
||||||
sliceLen := datav.Len()
|
|
||||||
if sliceLen != rv.Len() {
|
|
||||||
return e("expected array length %d; got TOML array of length %d",
|
|
||||||
rv.Len(), sliceLen)
|
|
||||||
}
|
|
||||||
return md.unifySliceArray(datav, rv)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (md *MetaData) unifySlice(data interface{}, rv reflect.Value) error {
|
|
||||||
datav := reflect.ValueOf(data)
|
|
||||||
if datav.Kind() != reflect.Slice {
|
|
||||||
if !datav.IsValid() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return badtype("slice", data)
|
|
||||||
}
|
|
||||||
n := datav.Len()
|
|
||||||
if rv.IsNil() || rv.Cap() < n {
|
|
||||||
rv.Set(reflect.MakeSlice(rv.Type(), n, n))
|
|
||||||
}
|
|
||||||
rv.SetLen(n)
|
|
||||||
return md.unifySliceArray(datav, rv)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (md *MetaData) unifySliceArray(data, rv reflect.Value) error {
|
|
||||||
sliceLen := data.Len()
|
|
||||||
for i := 0; i < sliceLen; i++ {
|
|
||||||
v := data.Index(i).Interface()
|
|
||||||
sliceval := indirect(rv.Index(i))
|
|
||||||
if err := md.unify(v, sliceval); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (md *MetaData) unifyDatetime(data interface{}, rv reflect.Value) error {
|
|
||||||
if _, ok := data.(time.Time); ok {
|
|
||||||
rv.Set(reflect.ValueOf(data))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return badtype("time.Time", data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (md *MetaData) unifyString(data interface{}, rv reflect.Value) error {
|
|
||||||
if s, ok := data.(string); ok {
|
|
||||||
rv.SetString(s)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return badtype("string", data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (md *MetaData) unifyFloat64(data interface{}, rv reflect.Value) error {
|
|
||||||
if num, ok := data.(float64); ok {
|
|
||||||
switch rv.Kind() {
|
|
||||||
case reflect.Float32:
|
|
||||||
fallthrough
|
|
||||||
case reflect.Float64:
|
|
||||||
rv.SetFloat(num)
|
|
||||||
default:
|
|
||||||
panic("bug")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return badtype("float", data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (md *MetaData) unifyInt(data interface{}, rv reflect.Value) error {
|
|
||||||
if num, ok := data.(int64); ok {
|
|
||||||
if rv.Kind() >= reflect.Int && rv.Kind() <= reflect.Int64 {
|
|
||||||
switch rv.Kind() {
|
|
||||||
case reflect.Int, reflect.Int64:
|
|
||||||
// No bounds checking necessary.
|
|
||||||
case reflect.Int8:
|
|
||||||
if num < math.MinInt8 || num > math.MaxInt8 {
|
|
||||||
return e("value %d is out of range for int8", num)
|
|
||||||
}
|
|
||||||
case reflect.Int16:
|
|
||||||
if num < math.MinInt16 || num > math.MaxInt16 {
|
|
||||||
return e("value %d is out of range for int16", num)
|
|
||||||
}
|
|
||||||
case reflect.Int32:
|
|
||||||
if num < math.MinInt32 || num > math.MaxInt32 {
|
|
||||||
return e("value %d is out of range for int32", num)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rv.SetInt(num)
|
|
||||||
} else if rv.Kind() >= reflect.Uint && rv.Kind() <= reflect.Uint64 {
|
|
||||||
unum := uint64(num)
|
|
||||||
switch rv.Kind() {
|
|
||||||
case reflect.Uint, reflect.Uint64:
|
|
||||||
// No bounds checking necessary.
|
|
||||||
case reflect.Uint8:
|
|
||||||
if num < 0 || unum > math.MaxUint8 {
|
|
||||||
return e("value %d is out of range for uint8", num)
|
|
||||||
}
|
|
||||||
case reflect.Uint16:
|
|
||||||
if num < 0 || unum > math.MaxUint16 {
|
|
||||||
return e("value %d is out of range for uint16", num)
|
|
||||||
}
|
|
||||||
case reflect.Uint32:
|
|
||||||
if num < 0 || unum > math.MaxUint32 {
|
|
||||||
return e("value %d is out of range for uint32", num)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rv.SetUint(unum)
|
|
||||||
} else {
|
|
||||||
panic("unreachable")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return badtype("integer", data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (md *MetaData) unifyBool(data interface{}, rv reflect.Value) error {
|
|
||||||
if b, ok := data.(bool); ok {
|
|
||||||
rv.SetBool(b)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return badtype("boolean", data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (md *MetaData) unifyAnything(data interface{}, rv reflect.Value) error {
|
|
||||||
rv.Set(reflect.ValueOf(data))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (md *MetaData) unifyText(data interface{}, v TextUnmarshaler) error {
|
|
||||||
var s string
|
|
||||||
switch sdata := data.(type) {
|
|
||||||
case TextMarshaler:
|
|
||||||
text, err := sdata.MarshalText()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s = string(text)
|
|
||||||
case fmt.Stringer:
|
|
||||||
s = sdata.String()
|
|
||||||
case string:
|
|
||||||
s = sdata
|
|
||||||
case bool:
|
|
||||||
s = fmt.Sprintf("%v", sdata)
|
|
||||||
case int64:
|
|
||||||
s = fmt.Sprintf("%d", sdata)
|
|
||||||
case float64:
|
|
||||||
s = fmt.Sprintf("%f", sdata)
|
|
||||||
default:
|
|
||||||
return badtype("primitive (string-like)", data)
|
|
||||||
}
|
|
||||||
if err := v.UnmarshalText([]byte(s)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// rvalue returns a reflect.Value of `v`. All pointers are resolved.
|
|
||||||
func rvalue(v interface{}) reflect.Value {
|
|
||||||
return indirect(reflect.ValueOf(v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// indirect returns the value pointed to by a pointer.
|
|
||||||
// Pointers are followed until the value is not a pointer.
|
|
||||||
// New values are allocated for each nil pointer.
|
|
||||||
//
|
|
||||||
// An exception to this rule is if the value satisfies an interface of
|
|
||||||
// interest to us (like encoding.TextUnmarshaler).
|
|
||||||
func indirect(v reflect.Value) reflect.Value {
|
|
||||||
if v.Kind() != reflect.Ptr {
|
|
||||||
if v.CanSet() {
|
|
||||||
pv := v.Addr()
|
|
||||||
if _, ok := pv.Interface().(TextUnmarshaler); ok {
|
|
||||||
return pv
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
if v.IsNil() {
|
|
||||||
v.Set(reflect.New(v.Type().Elem()))
|
|
||||||
}
|
|
||||||
return indirect(reflect.Indirect(v))
|
|
||||||
}
|
|
||||||
|
|
||||||
func isUnifiable(rv reflect.Value) bool {
|
|
||||||
if rv.CanSet() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if _, ok := rv.Interface().(TextUnmarshaler); ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func badtype(expected string, data interface{}) error {
|
|
||||||
return e("cannot load TOML value of type %T into a Go %s", data, expected)
|
|
||||||
}
|
|
||||||
121
vendor/github.com/BurntSushi/toml/decode_meta.go
generated
vendored
121
vendor/github.com/BurntSushi/toml/decode_meta.go
generated
vendored
@ -1,121 +0,0 @@
|
|||||||
package toml
|
|
||||||
|
|
||||||
import "strings"
|
|
||||||
|
|
||||||
// MetaData allows access to meta information about TOML data that may not
|
|
||||||
// be inferrable via reflection. In particular, whether a key has been defined
|
|
||||||
// and the TOML type of a key.
|
|
||||||
type MetaData struct {
|
|
||||||
mapping map[string]interface{}
|
|
||||||
types map[string]tomlType
|
|
||||||
keys []Key
|
|
||||||
decoded map[string]bool
|
|
||||||
context Key // Used only during decoding.
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsDefined returns true if the key given exists in the TOML data. The key
|
|
||||||
// should be specified hierarchially. e.g.,
|
|
||||||
//
|
|
||||||
// // access the TOML key 'a.b.c'
|
|
||||||
// IsDefined("a", "b", "c")
|
|
||||||
//
|
|
||||||
// IsDefined will return false if an empty key given. Keys are case sensitive.
|
|
||||||
func (md *MetaData) IsDefined(key ...string) bool {
|
|
||||||
if len(key) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
var hash map[string]interface{}
|
|
||||||
var ok bool
|
|
||||||
var hashOrVal interface{} = md.mapping
|
|
||||||
for _, k := range key {
|
|
||||||
if hash, ok = hashOrVal.(map[string]interface{}); !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if hashOrVal, ok = hash[k]; !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type returns a string representation of the type of the key specified.
|
|
||||||
//
|
|
||||||
// Type will return the empty string if given an empty key or a key that
|
|
||||||
// does not exist. Keys are case sensitive.
|
|
||||||
func (md *MetaData) Type(key ...string) string {
|
|
||||||
fullkey := strings.Join(key, ".")
|
|
||||||
if typ, ok := md.types[fullkey]; ok {
|
|
||||||
return typ.typeString()
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Key is the type of any TOML key, including key groups. Use (MetaData).Keys
|
|
||||||
// to get values of this type.
|
|
||||||
type Key []string
|
|
||||||
|
|
||||||
func (k Key) String() string {
|
|
||||||
return strings.Join(k, ".")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k Key) maybeQuotedAll() string {
|
|
||||||
var ss []string
|
|
||||||
for i := range k {
|
|
||||||
ss = append(ss, k.maybeQuoted(i))
|
|
||||||
}
|
|
||||||
return strings.Join(ss, ".")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k Key) maybeQuoted(i int) string {
|
|
||||||
quote := false
|
|
||||||
for _, c := range k[i] {
|
|
||||||
if !isBareKeyChar(c) {
|
|
||||||
quote = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if quote {
|
|
||||||
return "\"" + strings.Replace(k[i], "\"", "\\\"", -1) + "\""
|
|
||||||
}
|
|
||||||
return k[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k Key) add(piece string) Key {
|
|
||||||
newKey := make(Key, len(k)+1)
|
|
||||||
copy(newKey, k)
|
|
||||||
newKey[len(k)] = piece
|
|
||||||
return newKey
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keys returns a slice of every key in the TOML data, including key groups.
|
|
||||||
// Each key is itself a slice, where the first element is the top of the
|
|
||||||
// hierarchy and the last is the most specific.
|
|
||||||
//
|
|
||||||
// The list will have the same order as the keys appeared in the TOML data.
|
|
||||||
//
|
|
||||||
// All keys returned are non-empty.
|
|
||||||
func (md *MetaData) Keys() []Key {
|
|
||||||
return md.keys
|
|
||||||
}
|
|
||||||
|
|
||||||
// Undecoded returns all keys that have not been decoded in the order in which
|
|
||||||
// they appear in the original TOML document.
|
|
||||||
//
|
|
||||||
// This includes keys that haven't been decoded because of a Primitive value.
|
|
||||||
// Once the Primitive value is decoded, the keys will be considered decoded.
|
|
||||||
//
|
|
||||||
// Also note that decoding into an empty interface will result in no decoding,
|
|
||||||
// and so no keys will be considered decoded.
|
|
||||||
//
|
|
||||||
// In this sense, the Undecoded keys correspond to keys in the TOML document
|
|
||||||
// that do not have a concrete type in your representation.
|
|
||||||
func (md *MetaData) Undecoded() []Key {
|
|
||||||
undecoded := make([]Key, 0, len(md.keys))
|
|
||||||
for _, key := range md.keys {
|
|
||||||
if !md.decoded[key.String()] {
|
|
||||||
undecoded = append(undecoded, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undecoded
|
|
||||||
}
|
|
||||||
1447
vendor/github.com/BurntSushi/toml/decode_test.go
generated
vendored
1447
vendor/github.com/BurntSushi/toml/decode_test.go
generated
vendored
File diff suppressed because it is too large
Load Diff
27
vendor/github.com/BurntSushi/toml/doc.go
generated
vendored
27
vendor/github.com/BurntSushi/toml/doc.go
generated
vendored
@ -1,27 +0,0 @@
|
|||||||
/*
|
|
||||||
Package toml provides facilities for decoding and encoding TOML configuration
|
|
||||||
files via reflection. There is also support for delaying decoding with
|
|
||||||
the Primitive type, and querying the set of keys in a TOML document with the
|
|
||||||
MetaData type.
|
|
||||||
|
|
||||||
The specification implemented: https://github.com/toml-lang/toml
|
|
||||||
|
|
||||||
The sub-command github.com/BurntSushi/toml/cmd/tomlv can be used to verify
|
|
||||||
whether a file is a valid TOML document. It can also be used to print the
|
|
||||||
type of each key in a TOML document.
|
|
||||||
|
|
||||||
Testing
|
|
||||||
|
|
||||||
There are two important types of tests used for this package. The first is
|
|
||||||
contained inside '*_test.go' files and uses the standard Go unit testing
|
|
||||||
framework. These tests are primarily devoted to holistically testing the
|
|
||||||
decoder and encoder.
|
|
||||||
|
|
||||||
The second type of testing is used to verify the implementation's adherence
|
|
||||||
to the TOML specification. These tests have been factored into their own
|
|
||||||
project: https://github.com/BurntSushi/toml-test
|
|
||||||
|
|
||||||
The reason the tests are in a separate project is so that they can be used by
|
|
||||||
any implementation of TOML. Namely, it is language agnostic.
|
|
||||||
*/
|
|
||||||
package toml
|
|
||||||
568
vendor/github.com/BurntSushi/toml/encode.go
generated
vendored
568
vendor/github.com/BurntSushi/toml/encode.go
generated
vendored
@ -1,568 +0,0 @@
|
|||||||
package toml
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"reflect"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type tomlEncodeError struct{ error }
|
|
||||||
|
|
||||||
var (
|
|
||||||
errArrayMixedElementTypes = errors.New(
|
|
||||||
"toml: cannot encode array with mixed element types")
|
|
||||||
errArrayNilElement = errors.New(
|
|
||||||
"toml: cannot encode array with nil element")
|
|
||||||
errNonString = errors.New(
|
|
||||||
"toml: cannot encode a map with non-string key type")
|
|
||||||
errAnonNonStruct = errors.New(
|
|
||||||
"toml: cannot encode an anonymous field that is not a struct")
|
|
||||||
errArrayNoTable = errors.New(
|
|
||||||
"toml: TOML array element cannot contain a table")
|
|
||||||
errNoKey = errors.New(
|
|
||||||
"toml: top-level values must be Go maps or structs")
|
|
||||||
errAnything = errors.New("") // used in testing
|
|
||||||
)
|
|
||||||
|
|
||||||
var quotedReplacer = strings.NewReplacer(
|
|
||||||
"\t", "\\t",
|
|
||||||
"\n", "\\n",
|
|
||||||
"\r", "\\r",
|
|
||||||
"\"", "\\\"",
|
|
||||||
"\\", "\\\\",
|
|
||||||
)
|
|
||||||
|
|
||||||
// Encoder controls the encoding of Go values to a TOML document to some
|
|
||||||
// io.Writer.
|
|
||||||
//
|
|
||||||
// The indentation level can be controlled with the Indent field.
|
|
||||||
type Encoder struct {
|
|
||||||
// A single indentation level. By default it is two spaces.
|
|
||||||
Indent string
|
|
||||||
|
|
||||||
// hasWritten is whether we have written any output to w yet.
|
|
||||||
hasWritten bool
|
|
||||||
w *bufio.Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewEncoder returns a TOML encoder that encodes Go values to the io.Writer
|
|
||||||
// given. By default, a single indentation level is 2 spaces.
|
|
||||||
func NewEncoder(w io.Writer) *Encoder {
|
|
||||||
return &Encoder{
|
|
||||||
w: bufio.NewWriter(w),
|
|
||||||
Indent: " ",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encode writes a TOML representation of the Go value to the underlying
|
|
||||||
// io.Writer. If the value given cannot be encoded to a valid TOML document,
|
|
||||||
// then an error is returned.
|
|
||||||
//
|
|
||||||
// The mapping between Go values and TOML values should be precisely the same
|
|
||||||
// as for the Decode* functions. Similarly, the TextMarshaler interface is
|
|
||||||
// supported by encoding the resulting bytes as strings. (If you want to write
|
|
||||||
// arbitrary binary data then you will need to use something like base64 since
|
|
||||||
// TOML does not have any binary types.)
|
|
||||||
//
|
|
||||||
// When encoding TOML hashes (i.e., Go maps or structs), keys without any
|
|
||||||
// sub-hashes are encoded first.
|
|
||||||
//
|
|
||||||
// If a Go map is encoded, then its keys are sorted alphabetically for
|
|
||||||
// deterministic output. More control over this behavior may be provided if
|
|
||||||
// there is demand for it.
|
|
||||||
//
|
|
||||||
// Encoding Go values without a corresponding TOML representation---like map
|
|
||||||
// types with non-string keys---will cause an error to be returned. Similarly
|
|
||||||
// for mixed arrays/slices, arrays/slices with nil elements, embedded
|
|
||||||
// non-struct types and nested slices containing maps or structs.
|
|
||||||
// (e.g., [][]map[string]string is not allowed but []map[string]string is OK
|
|
||||||
// and so is []map[string][]string.)
|
|
||||||
func (enc *Encoder) Encode(v interface{}) error {
|
|
||||||
rv := eindirect(reflect.ValueOf(v))
|
|
||||||
if err := enc.safeEncode(Key([]string{}), rv); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return enc.w.Flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (enc *Encoder) safeEncode(key Key, rv reflect.Value) (err error) {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
if terr, ok := r.(tomlEncodeError); ok {
|
|
||||||
err = terr.error
|
|
||||||
return
|
|
||||||
}
|
|
||||||
panic(r)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
enc.encode(key, rv)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (enc *Encoder) encode(key Key, rv reflect.Value) {
|
|
||||||
// Special case. Time needs to be in ISO8601 format.
|
|
||||||
// Special case. If we can marshal the type to text, then we used that.
|
|
||||||
// Basically, this prevents the encoder for handling these types as
|
|
||||||
// generic structs (or whatever the underlying type of a TextMarshaler is).
|
|
||||||
switch rv.Interface().(type) {
|
|
||||||
case time.Time, TextMarshaler:
|
|
||||||
enc.keyEqElement(key, rv)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
k := rv.Kind()
|
|
||||||
switch k {
|
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
|
|
||||||
reflect.Int64,
|
|
||||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
|
|
||||||
reflect.Uint64,
|
|
||||||
reflect.Float32, reflect.Float64, reflect.String, reflect.Bool:
|
|
||||||
enc.keyEqElement(key, rv)
|
|
||||||
case reflect.Array, reflect.Slice:
|
|
||||||
if typeEqual(tomlArrayHash, tomlTypeOfGo(rv)) {
|
|
||||||
enc.eArrayOfTables(key, rv)
|
|
||||||
} else {
|
|
||||||
enc.keyEqElement(key, rv)
|
|
||||||
}
|
|
||||||
case reflect.Interface:
|
|
||||||
if rv.IsNil() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
enc.encode(key, rv.Elem())
|
|
||||||
case reflect.Map:
|
|
||||||
if rv.IsNil() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
enc.eTable(key, rv)
|
|
||||||
case reflect.Ptr:
|
|
||||||
if rv.IsNil() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
enc.encode(key, rv.Elem())
|
|
||||||
case reflect.Struct:
|
|
||||||
enc.eTable(key, rv)
|
|
||||||
default:
|
|
||||||
panic(e("unsupported type for key '%s': %s", key, k))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// eElement encodes any value that can be an array element (primitives and
|
|
||||||
// arrays).
|
|
||||||
func (enc *Encoder) eElement(rv reflect.Value) {
|
|
||||||
switch v := rv.Interface().(type) {
|
|
||||||
case time.Time:
|
|
||||||
// Special case time.Time as a primitive. Has to come before
|
|
||||||
// TextMarshaler below because time.Time implements
|
|
||||||
// encoding.TextMarshaler, but we need to always use UTC.
|
|
||||||
enc.wf(v.UTC().Format("2006-01-02T15:04:05Z"))
|
|
||||||
return
|
|
||||||
case TextMarshaler:
|
|
||||||
// Special case. Use text marshaler if it's available for this value.
|
|
||||||
if s, err := v.MarshalText(); err != nil {
|
|
||||||
encPanic(err)
|
|
||||||
} else {
|
|
||||||
enc.writeQuoted(string(s))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch rv.Kind() {
|
|
||||||
case reflect.Bool:
|
|
||||||
enc.wf(strconv.FormatBool(rv.Bool()))
|
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
|
|
||||||
reflect.Int64:
|
|
||||||
enc.wf(strconv.FormatInt(rv.Int(), 10))
|
|
||||||
case reflect.Uint, reflect.Uint8, reflect.Uint16,
|
|
||||||
reflect.Uint32, reflect.Uint64:
|
|
||||||
enc.wf(strconv.FormatUint(rv.Uint(), 10))
|
|
||||||
case reflect.Float32:
|
|
||||||
enc.wf(floatAddDecimal(strconv.FormatFloat(rv.Float(), 'f', -1, 32)))
|
|
||||||
case reflect.Float64:
|
|
||||||
enc.wf(floatAddDecimal(strconv.FormatFloat(rv.Float(), 'f', -1, 64)))
|
|
||||||
case reflect.Array, reflect.Slice:
|
|
||||||
enc.eArrayOrSliceElement(rv)
|
|
||||||
case reflect.Interface:
|
|
||||||
enc.eElement(rv.Elem())
|
|
||||||
case reflect.String:
|
|
||||||
enc.writeQuoted(rv.String())
|
|
||||||
default:
|
|
||||||
panic(e("unexpected primitive type: %s", rv.Kind()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// By the TOML spec, all floats must have a decimal with at least one
|
|
||||||
// number on either side.
|
|
||||||
func floatAddDecimal(fstr string) string {
|
|
||||||
if !strings.Contains(fstr, ".") {
|
|
||||||
return fstr + ".0"
|
|
||||||
}
|
|
||||||
return fstr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (enc *Encoder) writeQuoted(s string) {
|
|
||||||
enc.wf("\"%s\"", quotedReplacer.Replace(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (enc *Encoder) eArrayOrSliceElement(rv reflect.Value) {
|
|
||||||
length := rv.Len()
|
|
||||||
enc.wf("[")
|
|
||||||
for i := 0; i < length; i++ {
|
|
||||||
elem := rv.Index(i)
|
|
||||||
enc.eElement(elem)
|
|
||||||
if i != length-1 {
|
|
||||||
enc.wf(", ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
enc.wf("]")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) {
|
|
||||||
if len(key) == 0 {
|
|
||||||
encPanic(errNoKey)
|
|
||||||
}
|
|
||||||
for i := 0; i < rv.Len(); i++ {
|
|
||||||
trv := rv.Index(i)
|
|
||||||
if isNil(trv) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
panicIfInvalidKey(key)
|
|
||||||
enc.newline()
|
|
||||||
enc.wf("%s[[%s]]", enc.indentStr(key), key.maybeQuotedAll())
|
|
||||||
enc.newline()
|
|
||||||
enc.eMapOrStruct(key, trv)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (enc *Encoder) eTable(key Key, rv reflect.Value) {
|
|
||||||
panicIfInvalidKey(key)
|
|
||||||
if len(key) == 1 {
|
|
||||||
// Output an extra newline between top-level tables.
|
|
||||||
// (The newline isn't written if nothing else has been written though.)
|
|
||||||
enc.newline()
|
|
||||||
}
|
|
||||||
if len(key) > 0 {
|
|
||||||
enc.wf("%s[%s]", enc.indentStr(key), key.maybeQuotedAll())
|
|
||||||
enc.newline()
|
|
||||||
}
|
|
||||||
enc.eMapOrStruct(key, rv)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (enc *Encoder) eMapOrStruct(key Key, rv reflect.Value) {
|
|
||||||
switch rv := eindirect(rv); rv.Kind() {
|
|
||||||
case reflect.Map:
|
|
||||||
enc.eMap(key, rv)
|
|
||||||
case reflect.Struct:
|
|
||||||
enc.eStruct(key, rv)
|
|
||||||
default:
|
|
||||||
panic("eTable: unhandled reflect.Value Kind: " + rv.Kind().String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (enc *Encoder) eMap(key Key, rv reflect.Value) {
|
|
||||||
rt := rv.Type()
|
|
||||||
if rt.Key().Kind() != reflect.String {
|
|
||||||
encPanic(errNonString)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort keys so that we have deterministic output. And write keys directly
|
|
||||||
// underneath this key first, before writing sub-structs or sub-maps.
|
|
||||||
var mapKeysDirect, mapKeysSub []string
|
|
||||||
for _, mapKey := range rv.MapKeys() {
|
|
||||||
k := mapKey.String()
|
|
||||||
if typeIsHash(tomlTypeOfGo(rv.MapIndex(mapKey))) {
|
|
||||||
mapKeysSub = append(mapKeysSub, k)
|
|
||||||
} else {
|
|
||||||
mapKeysDirect = append(mapKeysDirect, k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var writeMapKeys = func(mapKeys []string) {
|
|
||||||
sort.Strings(mapKeys)
|
|
||||||
for _, mapKey := range mapKeys {
|
|
||||||
mrv := rv.MapIndex(reflect.ValueOf(mapKey))
|
|
||||||
if isNil(mrv) {
|
|
||||||
// Don't write anything for nil fields.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
enc.encode(key.add(mapKey), mrv)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
writeMapKeys(mapKeysDirect)
|
|
||||||
writeMapKeys(mapKeysSub)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (enc *Encoder) eStruct(key Key, rv reflect.Value) {
|
|
||||||
// Write keys for fields directly under this key first, because if we write
|
|
||||||
// a field that creates a new table, then all keys under it will be in that
|
|
||||||
// table (not the one we're writing here).
|
|
||||||
rt := rv.Type()
|
|
||||||
var fieldsDirect, fieldsSub [][]int
|
|
||||||
var addFields func(rt reflect.Type, rv reflect.Value, start []int)
|
|
||||||
addFields = func(rt reflect.Type, rv reflect.Value, start []int) {
|
|
||||||
for i := 0; i < rt.NumField(); i++ {
|
|
||||||
f := rt.Field(i)
|
|
||||||
// skip unexported fields
|
|
||||||
if f.PkgPath != "" && !f.Anonymous {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
frv := rv.Field(i)
|
|
||||||
if f.Anonymous {
|
|
||||||
t := f.Type
|
|
||||||
switch t.Kind() {
|
|
||||||
case reflect.Struct:
|
|
||||||
// Treat anonymous struct fields with
|
|
||||||
// tag names as though they are not
|
|
||||||
// anonymous, like encoding/json does.
|
|
||||||
if getOptions(f.Tag).name == "" {
|
|
||||||
addFields(t, frv, f.Index)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
case reflect.Ptr:
|
|
||||||
if t.Elem().Kind() == reflect.Struct &&
|
|
||||||
getOptions(f.Tag).name == "" {
|
|
||||||
if !frv.IsNil() {
|
|
||||||
addFields(t.Elem(), frv.Elem(), f.Index)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Fall through to the normal field encoding logic below
|
|
||||||
// for non-struct anonymous fields.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if typeIsHash(tomlTypeOfGo(frv)) {
|
|
||||||
fieldsSub = append(fieldsSub, append(start, f.Index...))
|
|
||||||
} else {
|
|
||||||
fieldsDirect = append(fieldsDirect, append(start, f.Index...))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addFields(rt, rv, nil)
|
|
||||||
|
|
||||||
var writeFields = func(fields [][]int) {
|
|
||||||
for _, fieldIndex := range fields {
|
|
||||||
sft := rt.FieldByIndex(fieldIndex)
|
|
||||||
sf := rv.FieldByIndex(fieldIndex)
|
|
||||||
if isNil(sf) {
|
|
||||||
// Don't write anything for nil fields.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
opts := getOptions(sft.Tag)
|
|
||||||
if opts.skip {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
keyName := sft.Name
|
|
||||||
if opts.name != "" {
|
|
||||||
keyName = opts.name
|
|
||||||
}
|
|
||||||
if opts.omitempty && isEmpty(sf) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if opts.omitzero && isZero(sf) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
enc.encode(key.add(keyName), sf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
writeFields(fieldsDirect)
|
|
||||||
writeFields(fieldsSub)
|
|
||||||
}
|
|
||||||
|
|
||||||
// tomlTypeName returns the TOML type name of the Go value's type. It is
|
|
||||||
// used to determine whether the types of array elements are mixed (which is
|
|
||||||
// forbidden). If the Go value is nil, then it is illegal for it to be an array
|
|
||||||
// element, and valueIsNil is returned as true.
|
|
||||||
|
|
||||||
// Returns the TOML type of a Go value. The type may be `nil`, which means
|
|
||||||
// no concrete TOML type could be found.
|
|
||||||
func tomlTypeOfGo(rv reflect.Value) tomlType {
|
|
||||||
if isNil(rv) || !rv.IsValid() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
switch rv.Kind() {
|
|
||||||
case reflect.Bool:
|
|
||||||
return tomlBool
|
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
|
|
||||||
reflect.Int64,
|
|
||||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
|
|
||||||
reflect.Uint64:
|
|
||||||
return tomlInteger
|
|
||||||
case reflect.Float32, reflect.Float64:
|
|
||||||
return tomlFloat
|
|
||||||
case reflect.Array, reflect.Slice:
|
|
||||||
if typeEqual(tomlHash, tomlArrayType(rv)) {
|
|
||||||
return tomlArrayHash
|
|
||||||
}
|
|
||||||
return tomlArray
|
|
||||||
case reflect.Ptr, reflect.Interface:
|
|
||||||
return tomlTypeOfGo(rv.Elem())
|
|
||||||
case reflect.String:
|
|
||||||
return tomlString
|
|
||||||
case reflect.Map:
|
|
||||||
return tomlHash
|
|
||||||
case reflect.Struct:
|
|
||||||
switch rv.Interface().(type) {
|
|
||||||
case time.Time:
|
|
||||||
return tomlDatetime
|
|
||||||
case TextMarshaler:
|
|
||||||
return tomlString
|
|
||||||
default:
|
|
||||||
return tomlHash
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
panic("unexpected reflect.Kind: " + rv.Kind().String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tomlArrayType returns the element type of a TOML array. The type returned
|
|
||||||
// may be nil if it cannot be determined (e.g., a nil slice or a zero length
|
|
||||||
// slize). This function may also panic if it finds a type that cannot be
|
|
||||||
// expressed in TOML (such as nil elements, heterogeneous arrays or directly
|
|
||||||
// nested arrays of tables).
|
|
||||||
func tomlArrayType(rv reflect.Value) tomlType {
|
|
||||||
if isNil(rv) || !rv.IsValid() || rv.Len() == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
firstType := tomlTypeOfGo(rv.Index(0))
|
|
||||||
if firstType == nil {
|
|
||||||
encPanic(errArrayNilElement)
|
|
||||||
}
|
|
||||||
|
|
||||||
rvlen := rv.Len()
|
|
||||||
for i := 1; i < rvlen; i++ {
|
|
||||||
elem := rv.Index(i)
|
|
||||||
switch elemType := tomlTypeOfGo(elem); {
|
|
||||||
case elemType == nil:
|
|
||||||
encPanic(errArrayNilElement)
|
|
||||||
case !typeEqual(firstType, elemType):
|
|
||||||
encPanic(errArrayMixedElementTypes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If we have a nested array, then we must make sure that the nested
|
|
||||||
// array contains ONLY primitives.
|
|
||||||
// This checks arbitrarily nested arrays.
|
|
||||||
if typeEqual(firstType, tomlArray) || typeEqual(firstType, tomlArrayHash) {
|
|
||||||
nest := tomlArrayType(eindirect(rv.Index(0)))
|
|
||||||
if typeEqual(nest, tomlHash) || typeEqual(nest, tomlArrayHash) {
|
|
||||||
encPanic(errArrayNoTable)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return firstType
|
|
||||||
}
|
|
||||||
|
|
||||||
type tagOptions struct {
|
|
||||||
skip bool // "-"
|
|
||||||
name string
|
|
||||||
omitempty bool
|
|
||||||
omitzero bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func getOptions(tag reflect.StructTag) tagOptions {
|
|
||||||
t := tag.Get("toml")
|
|
||||||
if t == "-" {
|
|
||||||
return tagOptions{skip: true}
|
|
||||||
}
|
|
||||||
var opts tagOptions
|
|
||||||
parts := strings.Split(t, ",")
|
|
||||||
opts.name = parts[0]
|
|
||||||
for _, s := range parts[1:] {
|
|
||||||
switch s {
|
|
||||||
case "omitempty":
|
|
||||||
opts.omitempty = true
|
|
||||||
case "omitzero":
|
|
||||||
opts.omitzero = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return opts
|
|
||||||
}
|
|
||||||
|
|
||||||
func isZero(rv reflect.Value) bool {
|
|
||||||
switch rv.Kind() {
|
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
||||||
return rv.Int() == 0
|
|
||||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
||||||
return rv.Uint() == 0
|
|
||||||
case reflect.Float32, reflect.Float64:
|
|
||||||
return rv.Float() == 0.0
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isEmpty(rv reflect.Value) bool {
|
|
||||||
switch rv.Kind() {
|
|
||||||
case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
|
|
||||||
return rv.Len() == 0
|
|
||||||
case reflect.Bool:
|
|
||||||
return !rv.Bool()
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (enc *Encoder) newline() {
|
|
||||||
if enc.hasWritten {
|
|
||||||
enc.wf("\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (enc *Encoder) keyEqElement(key Key, val reflect.Value) {
|
|
||||||
if len(key) == 0 {
|
|
||||||
encPanic(errNoKey)
|
|
||||||
}
|
|
||||||
panicIfInvalidKey(key)
|
|
||||||
enc.wf("%s%s = ", enc.indentStr(key), key.maybeQuoted(len(key)-1))
|
|
||||||
enc.eElement(val)
|
|
||||||
enc.newline()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (enc *Encoder) wf(format string, v ...interface{}) {
|
|
||||||
if _, err := fmt.Fprintf(enc.w, format, v...); err != nil {
|
|
||||||
encPanic(err)
|
|
||||||
}
|
|
||||||
enc.hasWritten = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (enc *Encoder) indentStr(key Key) string {
|
|
||||||
return strings.Repeat(enc.Indent, len(key)-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func encPanic(err error) {
|
|
||||||
panic(tomlEncodeError{err})
|
|
||||||
}
|
|
||||||
|
|
||||||
func eindirect(v reflect.Value) reflect.Value {
|
|
||||||
switch v.Kind() {
|
|
||||||
case reflect.Ptr, reflect.Interface:
|
|
||||||
return eindirect(v.Elem())
|
|
||||||
default:
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isNil(rv reflect.Value) bool {
|
|
||||||
switch rv.Kind() {
|
|
||||||
case reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
|
|
||||||
return rv.IsNil()
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func panicIfInvalidKey(key Key) {
|
|
||||||
for _, k := range key {
|
|
||||||
if len(k) == 0 {
|
|
||||||
encPanic(e("Key '%s' is not a valid table name. Key names "+
|
|
||||||
"cannot be empty.", key.maybeQuotedAll()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isValidKeyName(s string) bool {
|
|
||||||
return len(s) != 0
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user