Compare commits
No commits in common. "master" and "v0.1" have entirely different histories.
25
.github/workflows/e2e.yml
vendored
25
.github/workflows/e2e.yml
vendored
@ -1,25 +0,0 @@
|
|||||||
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
40
.github/workflows/go_cov.yml
vendored
@ -1,40 +0,0 @@
|
|||||||
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
29
.github/workflows/golangci-lint.yml
vendored
@ -1,29 +0,0 @@
|
|||||||
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
43
.github/workflows/release.yml
vendored
@ -1,43 +0,0 @@
|
|||||||
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 }}
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -3,5 +3,5 @@ acme-dns.db
|
|||||||
acme-dns.log
|
acme-dns.log
|
||||||
.vagrant
|
.vagrant
|
||||||
coverage.out
|
coverage.out
|
||||||
.idea/
|
vendor/*/
|
||||||
dist/
|
/vendor/**/.git
|
||||||
|
|||||||
@ -1,30 +0,0 @@
|
|||||||
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,35 +0,0 @@
|
|||||||
builds:
|
|
||||||
- binary: acme-dns
|
|
||||||
env:
|
|
||||||
- CGO_ENABLED=0
|
|
||||||
goos:
|
|
||||||
- linux
|
|
||||||
goarch:
|
|
||||||
- amd64
|
|
||||||
|
|
||||||
archives:
|
|
||||||
- id: tgz
|
|
||||||
format: tar.gz
|
|
||||||
files:
|
|
||||||
- LICENSE
|
|
||||||
- README.md
|
|
||||||
- Dockerfile
|
|
||||||
- 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}}"
|
|
||||||
|
|
||||||
15
.travis.yml
Normal file
15
.travis.yml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
language: go
|
||||||
|
go:
|
||||||
|
- 1.7
|
||||||
|
env:
|
||||||
|
- "PATH=/home/travis/gopath/bin:$PATH"
|
||||||
|
before_install:
|
||||||
|
- go get -u github.com/kardianos/govendor
|
||||||
|
- go get github.com/golang/lint/golint
|
||||||
|
- go get github.com/mattn/goveralls
|
||||||
|
- govendor sync
|
||||||
|
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
5
.vscode/settings.json
vendored
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"gopls": {
|
|
||||||
"formatting.local": "github.com/acme-dns/acme-dns"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
59
CHANGELOG.md
59
CHANGELOG.md
@ -1,59 +0,0 @@
|
|||||||
# 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
|
|
||||||
23
Dockerfile
23
Dockerfile
@ -1,23 +0,0 @@
|
|||||||
FROM golang:alpine AS builder
|
|
||||||
LABEL maintainer="joona@kuori.org"
|
|
||||||
|
|
||||||
RUN apk add --update git
|
|
||||||
|
|
||||||
ENV GOPATH /tmp/buildcache
|
|
||||||
RUN git clone https://github.com/joohoi/acme-dns /tmp/acme-dns
|
|
||||||
WORKDIR /tmp/acme-dns
|
|
||||||
RUN CGO_ENABLED=0 go build
|
|
||||||
|
|
||||||
FROM alpine:latest
|
|
||||||
|
|
||||||
WORKDIR /root/
|
|
||||||
COPY --from=builder /tmp/acme-dns .
|
|
||||||
RUN mkdir -p /etc/acme-dns
|
|
||||||
RUN mkdir -p /var/lib/acme-dns
|
|
||||||
RUN rm -rf ./config.cfg
|
|
||||||
RUN apk --no-cache add ca-certificates && update-ca-certificates
|
|
||||||
|
|
||||||
VOLUME ["/etc/acme-dns", "/var/lib/acme-dns"]
|
|
||||||
ENTRYPOINT ["./acme-dns"]
|
|
||||||
EXPOSE 53 80 443
|
|
||||||
EXPOSE 53/udp
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
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
|
|
||||||
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2016-2026 Joona Hoikkala
|
Copyright (c) 2016 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
|
||||||
|
|||||||
253
README.md
253
README.md
@ -8,11 +8,9 @@ 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 effects 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 effetcs 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)
|
||||||
@ -20,20 +18,16 @@ For longer explanation of the underlying issue and other proposed solutions, see
|
|||||||
- HTTP API automatically acquires and uses Let's Encrypt TLS certificate
|
- HTTP API automatically acquires and uses Let's Encrypt TLS certificate
|
||||||
- Limit /update API endpoint access to specific CIDR mask(s), defined in the /register request
|
- Limit /update API endpoint access to specific CIDR mask(s), defined in the /register request
|
||||||
- Supports SQLite & PostgreSQL as DB backends
|
- Supports SQLite & PostgreSQL as DB backends
|
||||||
- Rolling update of two TXT records to be able to answer to challenges for certificates that have both names: `yourdomain.tld` and `*.yourdomain.tld`, as both of the challenges point to the same subdomain.
|
|
||||||
- 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, or are using a service like acme-dns.io):
|
||||||
|
|
||||||
- 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 new DNS challenge values to an acme-dns server for the CA to validate from.
|
- Use your credentials to POST a new DNS challenge values to an acme-dns server for the CA to validate them off of.
|
||||||
- Crontab and forget.
|
- Crontab and forget.
|
||||||
|
|
||||||
## API
|
## API
|
||||||
@ -42,9 +36,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\_received\_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\_recieved\_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```
|
||||||
|
|
||||||
@ -54,8 +48,7 @@ With the credentials, you can update the TXT response in the service to match th
|
|||||||
"allowfrom": [
|
"allowfrom": [
|
||||||
"192.168.100.1/24",
|
"192.168.100.1/24",
|
||||||
"1.2.3.4/32",
|
"1.2.3.4/32",
|
||||||
"2002:c0a8:2a00::0/40"
|
"2002:c0a8:2a00::0/40",
|
||||||
]
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -65,8 +58,7 @@ With the credentials, you can update the TXT response in the service to match th
|
|||||||
{
|
{
|
||||||
"allowfrom": [
|
"allowfrom": [
|
||||||
"192.168.100.1/24",
|
"192.168.100.1/24",
|
||||||
"1.2.3.4/32",
|
"1.2.3.4/32"
|
||||||
"2002:c0a8:2a00::0/40"
|
|
||||||
],
|
],
|
||||||
"fulldomain": "8e5700ea-a4bf-41c7-8a77-e990661dcc6a.auth.acme-dns.io",
|
"fulldomain": "8e5700ea-a4bf-41c7-8a77-e990661dcc6a.auth.acme-dns.io",
|
||||||
"password": "htB9mR9DYgcu9bX_afHF62erXaH2TS7bg9KW3F7Z",
|
"password": "htB9mR9DYgcu9bX_afHF62erXaH2TS7bg9KW3F7Z",
|
||||||
@ -84,14 +76,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 received from registration | `X-Api-User: c36f50e8-4632-44f0-83fe-e070fef28a10` |
|
| X-Api-User | UUIDv4 username recieved from registration | `X-Api-User: c36f50e8-4632-44f0-83fe-e070fef28a10` |
|
||||||
| X-Api-Key | Password received from registration | `X-Api-Key: htB9mR9DYgcu9bX_afHF62erXaH2TS7bg9KW3F7Z` |
|
| X-Api-Key | Password recieved 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_received_from_the_ca___"
|
"txt": "___validation_token_recieved_from_the_ca___",
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -100,154 +92,63 @@ 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_received_from_the_ca___"
|
"txt": "___validation_token_recieved_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 the challenging 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 challengeing CA, making the instance able to request (and get issued) a TLS certificate for the domain that has CNAME pointing to it.
|
||||||
|
|
||||||
See the INSTALL section for information on how to do this.
|
Check out how in the INSTALL section.
|
||||||
|
|
||||||
|
## As a service
|
||||||
|
|
||||||
|
Acme-dns instance is running as a service for everyone wanting to get on in fast. You can find it at `auth.acme-dns.io`, so to get started, try:
|
||||||
|
```curl -X POST https://auth.acme-dns.io/register```
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1) Install [Go 1.13 or newer](https://golang.org/doc/install).
|
1) Install [Go](https://golang.org/doc/install)
|
||||||
|
|
||||||
2) Build acme-dns:
|
2) Clone this repo: `git clone https://github.com/joohoi/acme-dns $GOPATH/src/acme-dns`
|
||||||
```
|
|
||||||
git clone https://github.com/joohoi/acme-dns
|
|
||||||
cd acme-dns
|
|
||||||
export GOPATH=/tmp/acme-dns
|
|
||||||
go build
|
|
||||||
```
|
|
||||||
|
|
||||||
3) Move the built acme-dns binary to a directory in your $PATH, for example:
|
3) Install govendor. ‘go get -u github.com/kardianos/govendor’ . This is used for dependency handling.
|
||||||
`sudo mv acme-dns /usr/local/bin`
|
|
||||||
|
|
||||||
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.
|
4) Get dependencies: `cd $GOPATH/src/acme-dns` and `govendor sync`
|
||||||
|
|
||||||
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.
|
5) Build ACME-DNS: `go build`
|
||||||
|
|
||||||
1) Make sure that you have moved the configuration file to `/etc/acme-dns/config.cfg` so that acme-dns can access it globally.
|
6) Edit config.cfg to suit your needs (see [configuration](#configuration))
|
||||||
|
|
||||||
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).
|
7) 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.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
5) Run Docker, this example expects that you have `port = "80"` in your `config.cfg`:
|
|
||||||
```
|
|
||||||
docker run --rm --name acmedns \
|
|
||||||
-p 53:53 \
|
|
||||||
-p 53:53/udp \
|
|
||||||
-p 80:80 \
|
|
||||||
-v /path/to/your/config:/etc/acme-dns:ro \
|
|
||||||
-v /path/to/your/data:/var/lib/acme-dns \
|
|
||||||
-d joohoi/acme-dns
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker Compose
|
|
||||||
|
|
||||||
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`.
|
|
||||||
|
|
||||||
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`.
|
|
||||||
|
|
||||||
## 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. Note that systemd-resolved may reserve port 53 on 127.0.0.53
|
# dns interface
|
||||||
# In this case acme-dns will error out and you will need to define the listening interface
|
listen = ":53"
|
||||||
# for example: listen = "127.0.0.1:53"
|
# protocol, "udp", "udp4", "udp6" or "tcp", "tcp4", "tcp6"
|
||||||
listen = "127.0.0.1:53"
|
protocol = "udp"
|
||||||
# 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 = "auth.example.org"
|
nsname = "ns1.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 = [
|
||||||
# domain pointing to the public IP of your acme-dns server
|
# default A
|
||||||
"auth.example.org. A 198.51.100.1",
|
"auth.example.org. A 192.168.1.100",
|
||||||
# specify that auth.example.org will resolve any *.auth.example.org records
|
# A
|
||||||
"auth.example.org. NS auth.example.org.",
|
"ns1.auth.example.org. A 192.168.1.100",
|
||||||
|
"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
|
||||||
@ -256,34 +157,23 @@ 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
|
||||||
# Please note that the default Docker image uses path /var/lib/acme-dns/acme-dns.db for sqlite3
|
connection = "acme-dns.db"
|
||||||
connection = "/var/lib/acme-dns/acme-dns.db"
|
|
||||||
# connection = "postgres://user:password@localhost/acmedns_db"
|
# connection = "postgres://user:password@localhost/acmedns_db"
|
||||||
|
|
||||||
[api]
|
[api]
|
||||||
# listen ip eg. 127.0.0.1
|
# domain name to listen requests for, mandatory if using tls = "letsencrypt"
|
||||||
ip = "0.0.0.0"
|
api_domain = ""
|
||||||
# disable registration endpoint
|
|
||||||
disable_registration = false
|
|
||||||
# listen port, eg. 443 for default HTTPS
|
# listen port, eg. 443 for default HTTPS
|
||||||
port = "443"
|
port = "8080"
|
||||||
# possible values: "letsencrypt", "letsencryptstaging", "cert", "none"
|
# possible values: "letsencrypt", "cert", "none"
|
||||||
tls = "letsencryptstaging"
|
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 = [
|
||||||
"*"
|
"*"
|
||||||
]
|
]
|
||||||
# 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"
|
||||||
@ -294,62 +184,25 @@ 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"
|
||||||
```
|
```
|
||||||
|
|
||||||
## HTTPS API
|
## Changelog
|
||||||
|
- v0.1 Initial release
|
||||||
The RESTful acme-dns API can be exposed over HTTPS in two ways:
|
|
||||||
|
|
||||||
1. Using `tls = "letsencrypt"` and letting acme-dns issue its own certificate
|
|
||||||
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
|
||||||
|
|
||||||
- Logging to a file
|
- Logging to a file
|
||||||
- DNSSEC
|
|
||||||
- Want to see something implemented, make a feature request!
|
- Want to see something implemented, make a feature request!
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
acme-dns is open for contributions.
|
acme-dns is open for contributions.
|
||||||
If you have an idea for improvement, please open an new issue or feel free to write a PR!
|
If you have an improvement, please open a Pull Request.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
acme-dns is released under the [MIT License](https://www.opensource.org/licenses/MIT).
|
acme-dns is released under the [MIT License](http://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 :
|
||||||
|
|
||||||
# Vagrantfile for running integration tests with PostgreSQL
|
# Vagratnfile for running integration tests with PostgreSQL
|
||||||
|
|
||||||
VAGRANTFILE_API_VERSION = "2"
|
VAGRANTFILE_API_VERSION = "2"
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
[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
|
|
||||||
78
acmetxt.go
Normal file
78
acmetxt.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
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
|
||||||
|
LastActive int64
|
||||||
|
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 {
|
||||||
|
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
|
||||||
|
}
|
||||||
108
api.go
Normal file
108
api.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
log "github.com/Sirupsen/logrus"
|
||||||
|
"github.com/kataras/iris"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Serve is an authentication middlware function used to authenticate update requests
|
||||||
|
func (a authMiddleware) Serve(ctx *iris.Context) {
|
||||||
|
allowUpdate := false
|
||||||
|
usernameStr := ctx.RequestHeader("X-Api-User")
|
||||||
|
password := ctx.RequestHeader("X-Api-Key")
|
||||||
|
postData := ACMETxt{}
|
||||||
|
|
||||||
|
username, err := getValidUsername(usernameStr)
|
||||||
|
if err == nil && validKey(password) {
|
||||||
|
au, 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(password, "$2a$10$8JEFVNYYhLoBysjAxe2yBuXrkDojBQBkVpXEQgyQyjn43SvJ4vL36")
|
||||||
|
} else {
|
||||||
|
if correctPassword(password, au.Password) {
|
||||||
|
// Password ok
|
||||||
|
|
||||||
|
// Now test for the possibly limited ranges
|
||||||
|
if DNSConf.API.UseHeader {
|
||||||
|
ips := getIPListFromHeader(ctx.RequestHeader(DNSConf.API.HeaderName))
|
||||||
|
allowUpdate = au.allowedFromList(ips)
|
||||||
|
} else {
|
||||||
|
allowUpdate = au.allowedFrom(ctx.RequestIP())
|
||||||
|
}
|
||||||
|
|
||||||
|
if allowUpdate {
|
||||||
|
// Update is allowed from remote addr
|
||||||
|
if err := ctx.ReadJSON(&postData); err == nil {
|
||||||
|
if au.Subdomain == postData.Subdomain {
|
||||||
|
ctx.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// JSON error
|
||||||
|
ctx.JSON(iris.StatusBadRequest, iris.Map{"error": "bad data"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Wrong password
|
||||||
|
log.WithFields(log.Fields{"username": username}).Warning("Failed password check")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.JSON(iris.StatusUnauthorized, iris.Map{"error": "unauthorized"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func webRegisterPost(ctx *iris.Context) {
|
||||||
|
var regJSON iris.Map
|
||||||
|
var regStatus int
|
||||||
|
aTXT := ACMETxt{}
|
||||||
|
_ = ctx.ReadJSON(&aTXT)
|
||||||
|
// Create new user
|
||||||
|
nu, err := DB.Register(aTXT.AllowFrom)
|
||||||
|
if err != nil {
|
||||||
|
errstr := fmt.Sprintf("%v", err)
|
||||||
|
regJSON = iris.Map{"error": errstr}
|
||||||
|
regStatus = iris.StatusInternalServerError
|
||||||
|
log.WithFields(log.Fields{"error": err.Error()}).Debug("Error in registration")
|
||||||
|
} else {
|
||||||
|
regJSON = iris.Map{"username": nu.Username, "password": nu.Password, "fulldomain": nu.Subdomain + "." + DNSConf.General.Domain, "subdomain": nu.Subdomain, "allowfrom": nu.AllowFrom.ValidEntries()}
|
||||||
|
regStatus = iris.StatusCreated
|
||||||
|
|
||||||
|
log.WithFields(log.Fields{"user": nu.Username.String()}).Debug("Created new user")
|
||||||
|
}
|
||||||
|
ctx.JSON(regStatus, regJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
func webUpdatePost(ctx *iris.Context) {
|
||||||
|
// User auth done in middleware
|
||||||
|
a := ACMETxt{}
|
||||||
|
userStr := ctx.RequestHeader("X-API-User")
|
||||||
|
// Already checked in auth middlware
|
||||||
|
username, _ := getValidUsername(userStr)
|
||||||
|
// Already checked in auth middleware
|
||||||
|
_ = ctx.ReadJSON(&a)
|
||||||
|
a.Username = username
|
||||||
|
// Do update
|
||||||
|
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")
|
||||||
|
webUpdatePostError(ctx, errors.New("internal error"), iris.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.JSON(iris.StatusOK, iris.Map{"txt": a.Value})
|
||||||
|
} else {
|
||||||
|
log.WithFields(log.Fields{"subdomain": a.Subdomain, "txt": a.Value}).Debug("Bad data for subdomain")
|
||||||
|
webUpdatePostError(ctx, errors.New("bad data"), iris.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func webUpdatePostError(ctx *iris.Context, err error, status int) {
|
||||||
|
errStr := fmt.Sprintf("%v", err)
|
||||||
|
updJSON := iris.Map{"error": errStr}
|
||||||
|
ctx.JSON(status, updJSON)
|
||||||
|
}
|
||||||
262
api_test.go
Normal file
262
api_test.go
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/gavv/httpexpect"
|
||||||
|
"github.com/kataras/iris"
|
||||||
|
"github.com/kataras/iris/httptest"
|
||||||
|
"gopkg.in/DATA-DOG/go-sqlmock.v1"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupIris(t *testing.T, debug bool, noauth bool) *httpexpect.Expect {
|
||||||
|
iris.ResetDefault()
|
||||||
|
var dbcfg = dbsettings{
|
||||||
|
Engine: "sqlite3",
|
||||||
|
Connection: ":memory:"}
|
||||||
|
var httpapicfg = httpapi{
|
||||||
|
Domain: "",
|
||||||
|
Port: "8080",
|
||||||
|
TLS: "none",
|
||||||
|
CorsOrigins: []string{"*"},
|
||||||
|
UseHeader: false,
|
||||||
|
HeaderName: "X-Forwarded-For",
|
||||||
|
}
|
||||||
|
var dnscfg = DNSConfig{
|
||||||
|
API: httpapicfg,
|
||||||
|
Database: dbcfg,
|
||||||
|
}
|
||||||
|
DNSConf = dnscfg
|
||||||
|
var ForceAuth = authMiddleware{}
|
||||||
|
iris.Post("/register", webRegisterPost)
|
||||||
|
if noauth {
|
||||||
|
iris.Post("/update", webUpdatePost)
|
||||||
|
} else {
|
||||||
|
iris.Post("/update", ForceAuth.Serve, webUpdatePost)
|
||||||
|
}
|
||||||
|
httptestcfg := httptest.DefaultConfiguration()
|
||||||
|
httptestcfg.Debug = debug
|
||||||
|
return httptest.New(iris.Default, t, httptestcfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApiRegister(t *testing.T) {
|
||||||
|
e := setupIris(t, false, false)
|
||||||
|
e.POST("/register").Expect().
|
||||||
|
Status(iris.StatusCreated).
|
||||||
|
JSON().Object().
|
||||||
|
ContainsKey("fulldomain").
|
||||||
|
ContainsKey("subdomain").
|
||||||
|
ContainsKey("username").
|
||||||
|
ContainsKey("password").
|
||||||
|
NotContainsKey("error")
|
||||||
|
|
||||||
|
allowfrom := map[string][]interface{}{
|
||||||
|
"allowfrom": []interface{}{"123.123.123.123/32",
|
||||||
|
"1010.10.10.10/24",
|
||||||
|
"invalid"},
|
||||||
|
}
|
||||||
|
|
||||||
|
response := e.POST("/register").
|
||||||
|
WithJSON(allowfrom).
|
||||||
|
Expect().
|
||||||
|
Status(iris.StatusCreated).
|
||||||
|
JSON().Object().
|
||||||
|
ContainsKey("fulldomain").
|
||||||
|
ContainsKey("subdomain").
|
||||||
|
ContainsKey("username").
|
||||||
|
ContainsKey("password").
|
||||||
|
ContainsKey("allowfrom").
|
||||||
|
NotContainsKey("error")
|
||||||
|
|
||||||
|
response.Value("allowfrom").Array().Elements("123.123.123.123/32")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApiRegisterWithMockDB(t *testing.T) {
|
||||||
|
e := setupIris(t, false, false)
|
||||||
|
oldDb := DB.GetBackend()
|
||||||
|
db, mock, _ := sqlmock.New()
|
||||||
|
DB.SetBackend(db)
|
||||||
|
defer db.Close()
|
||||||
|
mock.ExpectBegin()
|
||||||
|
mock.ExpectPrepare("INSERT INTO records").WillReturnError(errors.New("error"))
|
||||||
|
e.POST("/register").Expect().
|
||||||
|
Status(iris.StatusInternalServerError).
|
||||||
|
JSON().Object().
|
||||||
|
ContainsKey("error")
|
||||||
|
DB.SetBackend(oldDb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApiUpdateWithoutCredentials(t *testing.T) {
|
||||||
|
e := setupIris(t, false, false)
|
||||||
|
e.POST("/update").Expect().
|
||||||
|
Status(iris.StatusUnauthorized).
|
||||||
|
JSON().Object().
|
||||||
|
ContainsKey("error").
|
||||||
|
NotContainsKey("txt")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApiUpdateWithCredentials(t *testing.T) {
|
||||||
|
validTxtData := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
|
||||||
|
updateJSON := map[string]interface{}{
|
||||||
|
"subdomain": "",
|
||||||
|
"txt": ""}
|
||||||
|
|
||||||
|
e := setupIris(t, false, false)
|
||||||
|
newUser, err := DB.Register(cidrslice{})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Could not create new user, got error [%v]", err)
|
||||||
|
}
|
||||||
|
// Valid data
|
||||||
|
updateJSON["subdomain"] = newUser.Subdomain
|
||||||
|
updateJSON["txt"] = validTxtData
|
||||||
|
|
||||||
|
e.POST("/update").
|
||||||
|
WithJSON(updateJSON).
|
||||||
|
WithHeader("X-Api-User", newUser.Username.String()).
|
||||||
|
WithHeader("X-Api-Key", newUser.Password).
|
||||||
|
Expect().
|
||||||
|
Status(iris.StatusOK).
|
||||||
|
JSON().Object().
|
||||||
|
ContainsKey("txt").
|
||||||
|
NotContainsKey("error").
|
||||||
|
ValueEqual("txt", validTxtData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApiUpdateWithCredentialsMockDB(t *testing.T) {
|
||||||
|
validTxtData := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
updateJSON := map[string]interface{}{
|
||||||
|
"subdomain": "",
|
||||||
|
"txt": ""}
|
||||||
|
|
||||||
|
// Valid data
|
||||||
|
updateJSON["subdomain"] = "a097455b-52cc-4569-90c8-7a4b97c6eba8"
|
||||||
|
updateJSON["txt"] = validTxtData
|
||||||
|
|
||||||
|
e := setupIris(t, false, true)
|
||||||
|
oldDb := DB.GetBackend()
|
||||||
|
db, mock, _ := sqlmock.New()
|
||||||
|
DB.SetBackend(db)
|
||||||
|
defer db.Close()
|
||||||
|
mock.ExpectBegin()
|
||||||
|
mock.ExpectPrepare("UPDATE records").WillReturnError(errors.New("error"))
|
||||||
|
e.POST("/update").
|
||||||
|
WithJSON(updateJSON).
|
||||||
|
Expect().
|
||||||
|
Status(iris.StatusInternalServerError).
|
||||||
|
JSON().Object().
|
||||||
|
ContainsKey("error")
|
||||||
|
DB.SetBackend(oldDb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApiManyUpdateWithCredentials(t *testing.T) {
|
||||||
|
// TODO: transfer to using httpexpect builder
|
||||||
|
// If test fails and more debug info is needed, use setupIris(t, true, false)
|
||||||
|
validTxtData := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
|
||||||
|
updateJSON := map[string]interface{}{
|
||||||
|
"subdomain": "",
|
||||||
|
"txt": ""}
|
||||||
|
|
||||||
|
e := setupIris(t, false, false)
|
||||||
|
// User without defined CIDR masks
|
||||||
|
newUser, err := DB.Register(cidrslice{})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Could not create new user, got error [%v]", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// User with defined allow from - CIDR masks, all invalid
|
||||||
|
// (httpexpect doesn't provide a way to mock remote ip)
|
||||||
|
newUserWithCIDR, err := DB.Register(cidrslice{"192.168.1.1/32", "invalid"})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Could not create new user with CIDR, got error [%v]", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Another user with valid CIDR mask to match the httpexpect default
|
||||||
|
newUserWithValidCIDR, err := DB.Register(cidrslice{"0.0.0.0/32", "invalid"})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Could not create new user with a valid CIDR, got error [%v]", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range []struct {
|
||||||
|
user string
|
||||||
|
pass string
|
||||||
|
subdomain string
|
||||||
|
txt interface{}
|
||||||
|
status int
|
||||||
|
}{
|
||||||
|
{"non-uuid-user", "tooshortpass", "non-uuid-subdomain", validTxtData, 401},
|
||||||
|
{"a097455b-52cc-4569-90c8-7a4b97c6eba8", "tooshortpass", "bb97455b-52cc-4569-90c8-7a4b97c6eba8", validTxtData, 401},
|
||||||
|
{"a097455b-52cc-4569-90c8-7a4b97c6eba8", "LongEnoughPassButNoUserExists___________", "bb97455b-52cc-4569-90c8-7a4b97c6eba8", validTxtData, 401},
|
||||||
|
{newUser.Username.String(), newUser.Password, "a097455b-52cc-4569-90c8-7a4b97c6eba8", validTxtData, 401},
|
||||||
|
{newUser.Username.String(), newUser.Password, newUser.Subdomain, "tooshortfortxt", 400},
|
||||||
|
{newUser.Username.String(), newUser.Password, newUser.Subdomain, 1234567890, 400},
|
||||||
|
{newUser.Username.String(), newUser.Password, newUser.Subdomain, validTxtData, 200},
|
||||||
|
{newUserWithCIDR.Username.String(), newUserWithCIDR.Password, newUserWithCIDR.Subdomain, validTxtData, 401},
|
||||||
|
{newUserWithValidCIDR.Username.String(), newUserWithValidCIDR.Password, newUserWithValidCIDR.Subdomain, validTxtData, 200},
|
||||||
|
{newUser.Username.String(), "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", newUser.Subdomain, validTxtData, 401},
|
||||||
|
} {
|
||||||
|
updateJSON = map[string]interface{}{
|
||||||
|
"subdomain": test.subdomain,
|
||||||
|
"txt": test.txt}
|
||||||
|
e.POST("/update").
|
||||||
|
WithJSON(updateJSON).
|
||||||
|
WithHeader("X-Api-User", test.user).
|
||||||
|
WithHeader("X-Api-Key", test.pass).
|
||||||
|
Expect().
|
||||||
|
Status(test.status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApiManyUpdateWithIpCheckHeaders(t *testing.T) {
|
||||||
|
|
||||||
|
updateJSON := map[string]interface{}{
|
||||||
|
"subdomain": "",
|
||||||
|
"txt": ""}
|
||||||
|
|
||||||
|
e := setupIris(t, false, false)
|
||||||
|
// Use header checks from default header (X-Forwarded-For)
|
||||||
|
DNSConf.API.UseHeader = true
|
||||||
|
// User without defined CIDR masks
|
||||||
|
newUser, err := DB.Register(cidrslice{})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Could not create new user, got error [%v]", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newUserWithCIDR, err := DB.Register(cidrslice{"192.168.1.2/32", "invalid"})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Could not create new user with CIDR, got error [%v]", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newUserWithIP6CIDR, err := DB.Register(cidrslice{"2002:c0a8::0/32"})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Could not create a new user with IP6 CIDR, got error [%v]", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range []struct {
|
||||||
|
user ACMETxt
|
||||||
|
headerValue string
|
||||||
|
status int
|
||||||
|
}{
|
||||||
|
{newUser, "whatever goes", 200},
|
||||||
|
{newUser, "10.0.0.1, 1.2.3.4 ,3.4.5.6", 200},
|
||||||
|
{newUserWithCIDR, "127.0.0.1", 401},
|
||||||
|
{newUserWithCIDR, "10.0.0.1, 10.0.0.2, 192.168.1.3", 401},
|
||||||
|
{newUserWithCIDR, "10.1.1.1 ,192.168.1.2, 8.8.8.8", 200},
|
||||||
|
{newUserWithIP6CIDR, "2002:c0a8:b4dc:0d3::0", 200},
|
||||||
|
{newUserWithIP6CIDR, "2002:c0a7:0ff::0", 401},
|
||||||
|
{newUserWithIP6CIDR, "2002:c0a8:d3ad:b33f:c0ff:33b4:dc0d:3b4d", 200},
|
||||||
|
} {
|
||||||
|
updateJSON = map[string]interface{}{
|
||||||
|
"subdomain": test.user.Subdomain,
|
||||||
|
"txt": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}
|
||||||
|
e.POST("/update").
|
||||||
|
WithJSON(updateJSON).
|
||||||
|
WithHeader("X-Api-User", test.user.Username.String()).
|
||||||
|
WithHeader("X-Api-Key", test.user.Password).
|
||||||
|
WithHeader("X-Forwarded-For", test.headerValue).
|
||||||
|
Expect().
|
||||||
|
Status(test.status)
|
||||||
|
}
|
||||||
|
DNSConf.API.UseHeader = false
|
||||||
|
}
|
||||||
55
config.cfg
55
config.cfg
@ -1,50 +1,45 @@
|
|||||||
[general]
|
[general]
|
||||||
# DNS interface. Note that systemd-resolved may reserve port 53 on 127.0.0.53
|
# dns interface
|
||||||
# In this case acme-dns will error out and you will need to define the listening interface
|
listen = ":53"
|
||||||
# for example: listen = "127.0.0.1:53"
|
# protocol, "udp", "udp4", "udp6" or "tcp", "tcp4", "tcp6"
|
||||||
listen = "127.0.0.1:53"
|
protocol = "udp"
|
||||||
# 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 = "auth.example.org"
|
nsname = "ns1.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 = [
|
||||||
# domain pointing to the public IP of your acme-dns server
|
# default A
|
||||||
"auth.example.org. A 198.51.100.1",
|
"auth.example.org. A 192.168.1.100",
|
||||||
# specify that auth.example.org will resolve any *.auth.example.org records
|
# A
|
||||||
"auth.example.org. NS auth.example.org.",
|
"ns1.auth.example.org. A 192.168.1.100",
|
||||||
|
"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, sqlite or postgres
|
# Database engine to use, sqlite3 or postgres
|
||||||
engine = "sqlite"
|
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
|
||||||
# Please note that the default Docker image uses path /var/lib/acme-dns/acme-dns.db for sqlite3
|
|
||||||
connection = "acme-dns.db"
|
connection = "acme-dns.db"
|
||||||
# connection = "postgres://user:password@localhost/acmedns_db"
|
# connection = "postgres://user:password@localhost/acmedns_db"
|
||||||
|
|
||||||
[api]
|
[api]
|
||||||
# listen ip eg. 127.0.0.1
|
# domain name to listen requests for, mandatory if using tls = "letsencrypt"
|
||||||
ip = "0.0.0.0"
|
api_domain = ""
|
||||||
# disable registration endpoint
|
|
||||||
disable_registration = false
|
|
||||||
# listen port, eg. 443 for default HTTPS
|
# listen port, eg. 443 for default HTTPS
|
||||||
port = "443"
|
port = "8080"
|
||||||
# possible values: "letsencrypt", "letsencryptstaging", "cert", "none"
|
# possible values: "letsencrypt", "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 +51,10 @@ header_name = "X-Forwarded-For"
|
|||||||
|
|
||||||
[logconfig]
|
[logconfig]
|
||||||
# logging level: "error", "warning", "info" or "debug"
|
# logging level: "error", "warning", "info" or "debug"
|
||||||
loglevel = "info"
|
loglevel = "debug"
|
||||||
# possible values: stdout, file
|
# possible values: stdout, TODO file & integrations
|
||||||
logtype = "stdout"
|
logtype = "stdout"
|
||||||
# file path for logfile
|
# file path for logfile TODO
|
||||||
logfile = "./acme-dns.log"
|
# logfile = "./acme-dns.log"
|
||||||
# format, either "json" or "text"
|
# format, either "json" or "text"
|
||||||
logformat = "json"
|
logformat = "text"
|
||||||
|
|||||||
212
db.go
Normal file
212
db.go
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"regexp"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/Sirupsen/logrus"
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
"github.com/satori/go.uuid"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var recordsTable = `
|
||||||
|
CREATE TABLE IF NOT EXISTS records(
|
||||||
|
Username TEXT UNIQUE NOT NULL PRIMARY KEY,
|
||||||
|
Password TEXT UNIQUE NOT NULL,
|
||||||
|
Subdomain TEXT UNIQUE NOT NULL,
|
||||||
|
Value TEXT,
|
||||||
|
LastActive INT,
|
||||||
|
AllowFrom TEXT
|
||||||
|
);`
|
||||||
|
|
||||||
|
// getSQLiteStmt replaces all PostgreSQL prepared statement placeholders (eg. $1, $2) with SQLite variant "?"
|
||||||
|
func getSQLiteStmt(s string) string {
|
||||||
|
re, _ := regexp.Compile("\\$[0-9]")
|
||||||
|
return re.ReplaceAllString(s, "?")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *acmedb) Init(engine string, connection string) error {
|
||||||
|
d.Lock()
|
||||||
|
defer d.Unlock()
|
||||||
|
db, err := sql.Open(engine, connection)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
d.DB = db
|
||||||
|
//d.DB.SetMaxOpenConns(1)
|
||||||
|
_, err = d.DB.Exec(recordsTable)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *acmedb) Register(afrom cidrslice) (ACMETxt, error) {
|
||||||
|
d.Lock()
|
||||||
|
defer d.Unlock()
|
||||||
|
a := newACMETxt()
|
||||||
|
a.AllowFrom = cidrslice(afrom.ValidEntries())
|
||||||
|
passwordHash, err := bcrypt.GenerateFromPassword([]byte(a.Password), 10)
|
||||||
|
timenow := time.Now().Unix()
|
||||||
|
regSQL := `
|
||||||
|
INSERT INTO records(
|
||||||
|
Username,
|
||||||
|
Password,
|
||||||
|
Subdomain,
|
||||||
|
Value,
|
||||||
|
LastActive,
|
||||||
|
AllowFrom)
|
||||||
|
values($1, $2, $3, '', $4, $5)`
|
||||||
|
if DNSConf.Database.Engine == "sqlite3" {
|
||||||
|
regSQL = getSQLiteStmt(regSQL)
|
||||||
|
}
|
||||||
|
sm, err := d.DB.Prepare(regSQL)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{"error": err.Error()}).Error("Database error in prepare")
|
||||||
|
return a, errors.New("SQL error")
|
||||||
|
}
|
||||||
|
defer sm.Close()
|
||||||
|
_, err = sm.Exec(a.Username.String(), passwordHash, a.Subdomain, timenow, a.AllowFrom.JSON())
|
||||||
|
if err != nil {
|
||||||
|
return a, err
|
||||||
|
}
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *acmedb) GetByUsername(u uuid.UUID) (ACMETxt, error) {
|
||||||
|
d.Lock()
|
||||||
|
defer d.Unlock()
|
||||||
|
var results []ACMETxt
|
||||||
|
getSQL := `
|
||||||
|
SELECT Username, Password, Subdomain, Value, LastActive, AllowFrom
|
||||||
|
FROM records
|
||||||
|
WHERE Username=$1 LIMIT 1
|
||||||
|
`
|
||||||
|
if DNSConf.Database.Engine == "sqlite3" {
|
||||||
|
getSQL = getSQLiteStmt(getSQL)
|
||||||
|
}
|
||||||
|
|
||||||
|
sm, err := d.DB.Prepare(getSQL)
|
||||||
|
if err != nil {
|
||||||
|
return ACMETxt{}, err
|
||||||
|
}
|
||||||
|
defer sm.Close()
|
||||||
|
rows, err := sm.Query(u.String())
|
||||||
|
if err != nil {
|
||||||
|
return ACMETxt{}, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
// It will only be one row though
|
||||||
|
for rows.Next() {
|
||||||
|
txt, err := getModelFromRow(rows)
|
||||||
|
if err != nil {
|
||||||
|
return ACMETxt{}, err
|
||||||
|
}
|
||||||
|
results = append(results, txt)
|
||||||
|
}
|
||||||
|
if len(results) > 0 {
|
||||||
|
return results[0], nil
|
||||||
|
}
|
||||||
|
return ACMETxt{}, errors.New("no user")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *acmedb) GetByDomain(domain string) ([]ACMETxt, error) {
|
||||||
|
d.Lock()
|
||||||
|
defer d.Unlock()
|
||||||
|
domain = sanitizeString(domain)
|
||||||
|
var a []ACMETxt
|
||||||
|
getSQL := `
|
||||||
|
SELECT Username, Password, Subdomain, Value, LastActive, AllowFrom
|
||||||
|
FROM records
|
||||||
|
WHERE Subdomain=$1 LIMIT 1
|
||||||
|
`
|
||||||
|
if DNSConf.Database.Engine == "sqlite3" {
|
||||||
|
getSQL = getSQLiteStmt(getSQL)
|
||||||
|
}
|
||||||
|
|
||||||
|
sm, err := d.DB.Prepare(getSQL)
|
||||||
|
if err != nil {
|
||||||
|
return a, err
|
||||||
|
}
|
||||||
|
defer sm.Close()
|
||||||
|
rows, err := sm.Query(domain)
|
||||||
|
if err != nil {
|
||||||
|
return a, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
txt, err := getModelFromRow(rows)
|
||||||
|
if err != nil {
|
||||||
|
return a, err
|
||||||
|
}
|
||||||
|
a = append(a, txt)
|
||||||
|
}
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *acmedb) Update(a ACMETxt) error {
|
||||||
|
d.Lock()
|
||||||
|
defer d.Unlock()
|
||||||
|
// Data in a is already sanitized
|
||||||
|
timenow := time.Now().Unix()
|
||||||
|
updSQL := `
|
||||||
|
UPDATE records SET Value=$1, LastActive=$2
|
||||||
|
WHERE Username=$3 AND Subdomain=$4
|
||||||
|
`
|
||||||
|
if DNSConf.Database.Engine == "sqlite3" {
|
||||||
|
updSQL = getSQLiteStmt(updSQL)
|
||||||
|
}
|
||||||
|
|
||||||
|
sm, err := d.DB.Prepare(updSQL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer sm.Close()
|
||||||
|
_, err = sm.Exec(a.Value, timenow, a.Username, a.Subdomain)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getModelFromRow(r *sql.Rows) (ACMETxt, error) {
|
||||||
|
txt := ACMETxt{}
|
||||||
|
afrom := ""
|
||||||
|
err := r.Scan(
|
||||||
|
&txt.Username,
|
||||||
|
&txt.Password,
|
||||||
|
&txt.Subdomain,
|
||||||
|
&txt.Value,
|
||||||
|
&txt.LastActive,
|
||||||
|
&afrom)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{"error": err.Error()}).Error("Row scan error")
|
||||||
|
}
|
||||||
|
|
||||||
|
cslice := cidrslice{}
|
||||||
|
err = json.Unmarshal([]byte(afrom), &cslice)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{"error": err.Error()}).Error("JSON unmarshall error")
|
||||||
|
}
|
||||||
|
txt.AllowFrom = cslice
|
||||||
|
return txt, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *acmedb) Close() {
|
||||||
|
d.DB.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *acmedb) GetBackend() *sql.DB {
|
||||||
|
return d.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *acmedb) SetBackend(backend *sql.DB) {
|
||||||
|
d.DB = backend
|
||||||
|
}
|
||||||
@ -1,15 +1,11 @@
|
|||||||
package database
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"database/sql/driver"
|
"database/sql/driver"
|
||||||
"errors"
|
"errors"
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/erikstmartin/go-testdb"
|
"github.com/erikstmartin/go-testdb"
|
||||||
"go.uber.org/zap"
|
"testing"
|
||||||
|
|
||||||
"github.com/joohoi/acme-dns/pkg/acmedns"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type testResult struct {
|
type testResult struct {
|
||||||
@ -25,38 +21,42 @@ func (r testResult) RowsAffected() (int64, error) {
|
|||||||
return r.affectedRows, nil
|
return r.affectedRows, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fakeConfigAndLogger() (acmedns.AcmeDnsConfig, *zap.SugaredLogger) {
|
func TestDBInit(t *testing.T) {
|
||||||
c := acmedns.AcmeDnsConfig{}
|
fakeDB := new(acmedb)
|
||||||
c.Database.Engine = "sqlite"
|
err := fakeDB.Init("notarealegine", "connectionstring")
|
||||||
c.Database.Connection = ":memory:"
|
if err == nil {
|
||||||
l := zap.NewNop().Sugar()
|
t.Errorf("Was expecting error, didn't get one.")
|
||||||
return c, l
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func fakeDB() acmedns.AcmednsDB {
|
testdb.SetExecWithArgsFunc(func(query string, args []driver.Value) (result driver.Result, err error) {
|
||||||
conf, logger := fakeConfigAndLogger()
|
return testResult{1, 0}, errors.New("Prepared query error")
|
||||||
db, _ := Init(&conf, logger)
|
})
|
||||||
return db
|
defer testdb.Reset()
|
||||||
|
|
||||||
|
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
|
||||||
DB := fakeDB()
|
_, err := DB.Register(cidrslice{})
|
||||||
_, 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 acmedns.Cidrslice
|
input cidrslice
|
||||||
output acmedns.Cidrslice
|
output cidrslice
|
||||||
}{
|
}{
|
||||||
{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{"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{"1.1.1./32", "1922.168.42.42/8", "1.1.1.1/33", "1.2.3.4/"}, acmedns.Cidrslice{}},
|
{cidrslice{"1.1.1./32", "1922.168.42.42/8", "1.1.1.1/33", "1.2.3.4/"}, cidrslice{}},
|
||||||
{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"}},
|
{cidrslice{"7.6.5.4/32", "invalid", "1.0.0.1/2"}, 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,19 +67,18 @@ 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 receive struct with [%d] entries in AllowFrom, but got [%d] records", i, len(test.output), len(user.AllowFrom))
|
t.Errorf("Test %d: Expected to recieve 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 receive struct with [%d] entries in AllowFrom, but got [%d] records", i, len(test.output), len(res.AllowFrom))
|
t.Errorf("Test %d: Expected to recieve 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(acmedns.Cidrslice{})
|
reg, err := DB.Register(cidrslice{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Registration failed, got error [%v]", err)
|
t.Errorf("Registration failed, got error [%v]", err)
|
||||||
}
|
}
|
||||||
@ -98,14 +97,13 @@ func TestGetByUsername(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// regUser password already is a bcrypt hash
|
// regUser password already is a bcrypt hash
|
||||||
if !acmedns.CorrectPassword(reg.Password, regUser.Password) {
|
if !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) {
|
||||||
DB := fakeDB()
|
reg, _ := DB.Register(cidrslice{})
|
||||||
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)
|
||||||
@ -120,15 +118,14 @@ func TestPrepareErrors(t *testing.T) {
|
|||||||
t.Errorf("Expected error, but didn't get one")
|
t.Errorf("Expected error, but didn't get one")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = DB.GetTXTForDomain(reg.Subdomain)
|
_, err = DB.GetByDomain(reg.Subdomain)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("Expected error, but didn't get one")
|
t.Errorf("Expected error, but didn't get one")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestQueryExecErrors(t *testing.T) {
|
func TestQueryExecErrors(t *testing.T) {
|
||||||
DB := fakeDB()
|
reg, _ := DB.Register(cidrslice{})
|
||||||
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")
|
||||||
})
|
})
|
||||||
@ -154,17 +151,17 @@ func TestQueryExecErrors(t *testing.T) {
|
|||||||
t.Errorf("Expected error from exec, but got none")
|
t.Errorf("Expected error from exec, but got none")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = DB.GetTXTForDomain(reg.Subdomain)
|
_, err = DB.GetByDomain(reg.Subdomain)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
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(acmedns.Cidrslice{})
|
_, err = DB.Register(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.ACMETxtPost)
|
err = DB.Update(reg)
|
||||||
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")
|
||||||
}
|
}
|
||||||
@ -172,8 +169,7 @@ func TestQueryExecErrors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestQueryScanErrors(t *testing.T) {
|
func TestQueryScanErrors(t *testing.T) {
|
||||||
DB := fakeDB()
|
reg, _ := DB.Register(cidrslice{})
|
||||||
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")
|
||||||
@ -199,11 +195,15 @@ func TestQueryScanErrors(t *testing.T) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("Expected error from scan in, but got none")
|
t.Errorf("Expected error from scan in, but got none")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err = DB.GetByDomain(reg.Subdomain)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected error from scan in GetByDomain, but got none")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBadDBValues(t *testing.T) {
|
func TestBadDBValues(t *testing.T) {
|
||||||
DB := fakeDB()
|
reg, _ := DB.Register(cidrslice{})
|
||||||
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"}
|
||||||
@ -226,65 +226,54 @@ func TestBadDBValues(t *testing.T) {
|
|||||||
t.Errorf("Expected error from scan in, but got none")
|
t.Errorf("Expected error from scan in, but got none")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = DB.GetTXTForDomain(reg.Subdomain)
|
_, err = DB.GetByDomain(reg.Subdomain)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("Expected error from scan in GetByDomain, but got none")
|
t.Errorf("Expected error from scan in GetByDomain, but got none")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetTXTForDomain(t *testing.T) {
|
func TestGetByDomain(t *testing.T) {
|
||||||
DB := fakeDB()
|
var regDomain = ACMETxt{}
|
||||||
|
|
||||||
// Create reg to refer to
|
// Create reg to refer to
|
||||||
reg, err := DB.Register(acmedns.Cidrslice{})
|
reg, err := DB.Register(cidrslice{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Registration failed, got error [%v]", err)
|
t.Errorf("Registration failed, got error [%v]", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
txtval1 := "___validation_token_received_from_the_ca___"
|
regDomainSlice, err := DB.GetByDomain(reg.Subdomain)
|
||||||
txtval2 := "___validation_token_received_YEAH_the_ca___"
|
|
||||||
|
|
||||||
reg.Value = txtval1
|
|
||||||
_ = DB.Update(reg.ACMETxtPost)
|
|
||||||
|
|
||||||
reg.Value = txtval2
|
|
||||||
_ = DB.Update(reg.ACMETxtPost)
|
|
||||||
|
|
||||||
regDomainSlice, err := DB.GetTXTForDomain(reg.Subdomain)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Could not get test user, got error [%v]", err)
|
t.Errorf("Could not get test user, got error [%v]", err)
|
||||||
}
|
}
|
||||||
if len(regDomainSlice) == 0 {
|
if len(regDomainSlice) == 0 {
|
||||||
t.Errorf("No rows returned for GetTXTForDomain [%s]", reg.Subdomain)
|
t.Errorf("No rows returned for GetByDomain [%s]", reg.Subdomain)
|
||||||
|
} else {
|
||||||
|
regDomain = regDomainSlice[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
var val1found = false
|
if reg.Username != regDomain.Username {
|
||||||
var val2found = false
|
t.Errorf("GetByUsername username [%q] did not match the original [%q]", regDomain.Username, reg.Username)
|
||||||
for _, v := range regDomainSlice {
|
|
||||||
if v == txtval1 {
|
|
||||||
val1found = true
|
|
||||||
}
|
|
||||||
if v == txtval2 {
|
|
||||||
val2found = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !val1found {
|
|
||||||
t.Errorf("No TXT value found for val1")
|
if reg.Subdomain != regDomain.Subdomain {
|
||||||
|
t.Errorf("GetByUsername subdomain [%q] did not match the original [%q]", regDomain.Subdomain, reg.Subdomain)
|
||||||
}
|
}
|
||||||
if !val2found {
|
|
||||||
t.Errorf("No TXT value found for val2")
|
// regDomain password already is a bcrypt hash
|
||||||
|
if !correctPassword(reg.Password, regDomain.Password) {
|
||||||
|
t.Errorf("The password [%s] does not match the hash [%s]", reg.Password, regDomain.Password)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not found
|
// Not found
|
||||||
regNotfound, _ := DB.GetTXTForDomain("does-not-exist")
|
regNotfound, _ := DB.GetByDomain("does-not-exist")
|
||||||
if len(regNotfound) > 0 {
|
if len(regNotfound) > 0 {
|
||||||
t.Errorf("No records should be returned.")
|
t.Errorf("No records should be returned.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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(acmedns.Cidrslice{})
|
reg, err := DB.Register(cidrslice{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Registration failed, got error [%v]", err)
|
t.Errorf("Registration failed, got error [%v]", err)
|
||||||
}
|
}
|
||||||
@ -301,8 +290,16 @@ func TestUpdate(t *testing.T) {
|
|||||||
regUser.Password = "nevergonnagiveyouup"
|
regUser.Password = "nevergonnagiveyouup"
|
||||||
regUser.Value = validTXT
|
regUser.Value = validTXT
|
||||||
|
|
||||||
err = DB.Update(regUser.ACMETxtPost)
|
err = DB.Update(regUser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("DB Update failed, got error: [%v]", err)
|
t.Errorf("DB Update failed, got error: [%v]", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updUser, err := DB.GetByUsername(regUser.Username)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("GetByUsername threw error [%v]", err)
|
||||||
|
}
|
||||||
|
if updUser.Value != validTXT {
|
||||||
|
t.Errorf("Update failed, fetched value [%s] does not match the update value [%s]", updUser.Value, validTXT)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
106
dns.go
Normal file
106
dns.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
log "github.com/Sirupsen/logrus"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"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.GetByDomain(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.Value) > 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.Value)
|
||||||
|
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
Normal file
195
dns_test.go
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,15 +0,0 @@
|
|||||||
version: '2'
|
|
||||||
services:
|
|
||||||
acmedns:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
image: joohoi/acme-dns:latest
|
|
||||||
ports:
|
|
||||||
- "443:443"
|
|
||||||
- "53:53"
|
|
||||||
- "53:53/udp"
|
|
||||||
- "80:80"
|
|
||||||
volumes:
|
|
||||||
- ./config:/etc/acme-dns:ro
|
|
||||||
- ./data:/var/lib/acme-dns
|
|
||||||
73
go.mod
73
go.mod
@ -1,73 +0,0 @@
|
|||||||
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
271
go.sum
@ -1,271 +0,0 @@
|
|||||||
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=
|
|
||||||
93
main.go
93
main.go
@ -1,55 +1,68 @@
|
|||||||
|
//+build !test
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
log "github.com/Sirupsen/logrus"
|
||||||
"fmt"
|
"github.com/iris-contrib/middleware/cors"
|
||||||
|
"github.com/kataras/iris"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/joohoi/acme-dns/pkg/acmedns"
|
|
||||||
"github.com/joohoi/acme-dns/pkg/api"
|
|
||||||
"github.com/joohoi/acme-dns/pkg/database"
|
|
||||||
"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
|
||||||
var err error
|
configTmp := readConfig("config.cfg")
|
||||||
var logger *zap.Logger
|
DNSConf = configTmp
|
||||||
config, usedConfigFile, err := acmedns.ReadConfig(*configPtr, "./config.cfg")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error: %s\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
logger, err = acmedns.SetupLogging(config)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Could not set up logging: %s\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
// Make sure to flush the zap logger buffer before exiting
|
|
||||||
defer logger.Sync() //nolint:all
|
|
||||||
sugar := logger.Sugar()
|
|
||||||
|
|
||||||
sugar.Infow("Using config file",
|
setupLogging(DNSConf.Logconfig.Format, DNSConf.Logconfig.Level)
|
||||||
"file", usedConfigFile)
|
|
||||||
sugar.Info("Starting up")
|
// Read the default records in
|
||||||
db, err := database.Init(&config, sugar)
|
RR.Parse(DNSConf.General)
|
||||||
// Error channel for servers
|
|
||||||
errChan := make(chan error, 1)
|
// Open database
|
||||||
api := api.Init(&config, db, sugar, errChan)
|
newDB := new(acmedb)
|
||||||
dnsservers := nameserver.InitAndStart(&config, db, sugar, errChan)
|
err := newDB.Init(DNSConf.Database.Engine, DNSConf.Database.Connection)
|
||||||
go api.Start(dnsservers)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sugar.Error(err)
|
log.Errorf("Could not open database [%v]", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
for {
|
DB = newDB
|
||||||
err = <-errChan
|
defer DB.Close()
|
||||||
|
|
||||||
|
// DNS server
|
||||||
|
startDNS(DNSConf.General.Listen, DNSConf.General.Proto)
|
||||||
|
|
||||||
|
// HTTP API
|
||||||
|
startHTTPAPI()
|
||||||
|
|
||||||
|
log.Debugf("Shutting down...")
|
||||||
|
}
|
||||||
|
|
||||||
|
func startHTTPAPI() {
|
||||||
|
api := iris.New()
|
||||||
|
api.Config.DisableBanner = true
|
||||||
|
crs := cors.New(cors.Options{
|
||||||
|
AllowedOrigins: DNSConf.API.CorsOrigins,
|
||||||
|
AllowedMethods: []string{"GET", "POST"},
|
||||||
|
OptionsPassthrough: false,
|
||||||
|
Debug: DNSConf.General.Debug,
|
||||||
|
})
|
||||||
|
api.Use(crs)
|
||||||
|
var ForceAuth = authMiddleware{}
|
||||||
|
api.Post("/register", webRegisterPost)
|
||||||
|
api.Post("/update", ForceAuth.Serve, webUpdatePost)
|
||||||
|
switch DNSConf.API.TLS {
|
||||||
|
case "letsencrypt":
|
||||||
|
listener, err := iris.LETSENCRYPTPROD(DNSConf.API.Domain)
|
||||||
|
err = api.Serve(listener)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sugar.Fatal(err)
|
log.Errorf("Error in HTTP server [%v]", err)
|
||||||
}
|
}
|
||||||
|
case "cert":
|
||||||
|
host := DNSConf.API.Domain + ":" + DNSConf.API.Port
|
||||||
|
api.ListenTLS(host, DNSConf.API.TLSCertFullchain, DNSConf.API.TLSCertPrivkey)
|
||||||
|
default:
|
||||||
|
host := DNSConf.API.Domain + ":" + DNSConf.API.Port
|
||||||
|
api.Listen(host)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
96
main_test.go
Normal file
96
main_test.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
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(DNSConf.General)
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
newDb := new(acmedb)
|
||||||
|
if *postgres {
|
||||||
|
DNSConf.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 {
|
||||||
|
DNSConf.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,
|
||||||
|
}
|
||||||
|
|
||||||
|
DNSConf = 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
|
||||||
|
}
|
||||||
@ -1,47 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
[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"
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
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"`
|
|
||||||
}
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,326 +0,0 @@
|
|||||||
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
135
pkg/api/api.go
@ -1,135 +0,0 @@
|
|||||||
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,533 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"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/google/uuid"
|
|
||||||
"github.com/julienschmidt/httprouter"
|
|
||||||
"github.com/rs/cors"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
func noAuth(update httprouter.Handle) httprouter.Handle {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
|
||||||
postData := acmedns.ACMETxt{}
|
|
||||||
uname := r.Header.Get("X-Api-User")
|
|
||||||
passwd := r.Header.Get("X-Api-Key")
|
|
||||||
|
|
||||||
dec := json.NewDecoder(r.Body)
|
|
||||||
_ = dec.Decode(&postData)
|
|
||||||
// Set user info to the decoded ACMETxt object
|
|
||||||
postData.Username, _ = uuid.Parse(uname)
|
|
||||||
postData.Password = passwd
|
|
||||||
// Set the ACMETxt struct to context to pull in from update function
|
|
||||||
ctx := r.Context()
|
|
||||||
ctx = context.WithValue(ctx, ACMETxtKey, postData)
|
|
||||||
r = r.WithContext(ctx)
|
|
||||||
update(w, r, p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getExpect(t *testing.T, server *httptest.Server) *httpexpect.Expect {
|
|
||||||
return httpexpect.WithConfig(httpexpect.Config{
|
|
||||||
BaseURL: server.URL,
|
|
||||||
Reporter: httpexpect.NewAssertReporter(t),
|
|
||||||
Printers: []httpexpect.Printer{
|
|
||||||
httpexpect.NewCurlPrinter(t),
|
|
||||||
httpexpect.NewDebugPrinter(t, true),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupRouter(debug bool, noauth bool) (http.Handler, AcmednsAPI, acmedns.AcmednsDB) {
|
|
||||||
api := httprouter.New()
|
|
||||||
config, logger := fakeConfigAndLogger()
|
|
||||||
config.API.Domain = ""
|
|
||||||
config.API.Port = "8080"
|
|
||||||
config.API.TLS = acmedns.ApiTlsProviderNone
|
|
||||||
config.API.CorsOrigins = []string{"*"}
|
|
||||||
config.API.UseHeader = true
|
|
||||||
config.API.HeaderName = "X-Forwarded-For"
|
|
||||||
|
|
||||||
db, _ := database.Init(&config, logger)
|
|
||||||
errChan := make(chan error, 1)
|
|
||||||
adnsapi := Init(&config, db, logger, errChan)
|
|
||||||
c := cors.New(cors.Options{
|
|
||||||
AllowedOrigins: config.API.CorsOrigins,
|
|
||||||
AllowedMethods: []string{"GET", "POST"},
|
|
||||||
OptionsPassthrough: false,
|
|
||||||
Debug: config.General.Debug,
|
|
||||||
})
|
|
||||||
api.POST("/register", adnsapi.webRegisterPost)
|
|
||||||
api.GET("/health", adnsapi.healthCheck)
|
|
||||||
if noauth {
|
|
||||||
api.POST("/update", noAuth(adnsapi.webUpdatePost))
|
|
||||||
} else {
|
|
||||||
api.POST("/update", adnsapi.Auth(adnsapi.webUpdatePost))
|
|
||||||
}
|
|
||||||
return c.Handler(api), adnsapi, db
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestApiRegister(t *testing.T) {
|
|
||||||
router, _, _ := setupRouter(false, false)
|
|
||||||
server := httptest.NewServer(router)
|
|
||||||
defer server.Close()
|
|
||||||
e := getExpect(t, server)
|
|
||||||
e.POST("/register").Expect().
|
|
||||||
Status(http.StatusCreated).
|
|
||||||
JSON().Object().
|
|
||||||
ContainsKey("fulldomain").
|
|
||||||
ContainsKey("subdomain").
|
|
||||||
ContainsKey("username").
|
|
||||||
ContainsKey("password").
|
|
||||||
NotContainsKey("error")
|
|
||||||
|
|
||||||
allowfrom := map[string][]interface{}{
|
|
||||||
"allowfrom": {"123.123.123.123/32",
|
|
||||||
"2001:db8:a0b:12f0::1/32",
|
|
||||||
"[::1]/64",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
response := e.POST("/register").
|
|
||||||
WithJSON(allowfrom).
|
|
||||||
Expect().
|
|
||||||
Status(http.StatusCreated).
|
|
||||||
JSON().Object().
|
|
||||||
ContainsKey("fulldomain").
|
|
||||||
ContainsKey("subdomain").
|
|
||||||
ContainsKey("username").
|
|
||||||
ContainsKey("password").
|
|
||||||
ContainsKey("allowfrom").
|
|
||||||
NotContainsKey("error")
|
|
||||||
|
|
||||||
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) {
|
|
||||||
router, _, _ := setupRouter(false, false)
|
|
||||||
server := httptest.NewServer(router)
|
|
||||||
defer server.Close()
|
|
||||||
e := getExpect(t, server)
|
|
||||||
|
|
||||||
malPayloads := []string{
|
|
||||||
"{\"allowfrom': '1.1.1.1/32'}",
|
|
||||||
"\"allowfrom\": \"1.1.1.1/32\"",
|
|
||||||
"{\"allowfrom\": \"[1.1.1.1/32]\"",
|
|
||||||
"\"allowfrom\": \"1.1.1.1/32\"}",
|
|
||||||
"{allowfrom: \"1.2.3.4\"}",
|
|
||||||
"{allowfrom: [1.2.3.4]}",
|
|
||||||
"whatever that's not a json payload",
|
|
||||||
}
|
|
||||||
for _, test := range malPayloads {
|
|
||||||
e.POST("/register").
|
|
||||||
WithBytes([]byte(test)).
|
|
||||||
Expect().
|
|
||||||
Status(http.StatusBadRequest).
|
|
||||||
JSON().Object().
|
|
||||||
ContainsKey("error").
|
|
||||||
NotContainsKey("subdomain").
|
|
||||||
NotContainsKey("username")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestApiRegisterWithMockDB(t *testing.T) {
|
|
||||||
router, _, db := setupRouter(false, false)
|
|
||||||
server := httptest.NewServer(router)
|
|
||||||
defer server.Close()
|
|
||||||
e := getExpect(t, server)
|
|
||||||
oldDb := db.GetBackend()
|
|
||||||
mdb, mock, _ := sqlmock.New()
|
|
||||||
db.SetBackend(mdb)
|
|
||||||
defer db.Close()
|
|
||||||
mock.ExpectBegin()
|
|
||||||
mock.ExpectPrepare("INSERT INTO records").WillReturnError(errors.New("error"))
|
|
||||||
e.POST("/register").Expect().
|
|
||||||
Status(http.StatusInternalServerError).
|
|
||||||
JSON().Object().
|
|
||||||
ContainsKey("error")
|
|
||||||
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) {
|
|
||||||
router, _, _ := setupRouter(false, false)
|
|
||||||
server := httptest.NewServer(router)
|
|
||||||
defer server.Close()
|
|
||||||
e := getExpect(t, server)
|
|
||||||
e.POST("/update").Expect().
|
|
||||||
Status(http.StatusUnauthorized).
|
|
||||||
JSON().Object().
|
|
||||||
ContainsKey("error").
|
|
||||||
NotContainsKey("txt")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestApiUpdateWithCredentials(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)
|
|
||||||
}
|
|
||||||
// Valid data
|
|
||||||
updateJSON["subdomain"] = newUser.Subdomain
|
|
||||||
updateJSON["txt"] = validTxtData
|
|
||||||
e.POST("/update").
|
|
||||||
WithJSON(updateJSON).
|
|
||||||
WithHeader("X-Api-User", newUser.Username.String()).
|
|
||||||
WithHeader("X-Api-Key", newUser.Password).
|
|
||||||
Expect().
|
|
||||||
Status(http.StatusOK).
|
|
||||||
JSON().Object().
|
|
||||||
ContainsKey("txt").
|
|
||||||
NotContainsKey("error").
|
|
||||||
ValueEqual("txt", validTxtData)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestApiUpdateWithCredentialsMockDB(t *testing.T) {
|
|
||||||
validTxtData := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
||||||
updateJSON := map[string]interface{}{
|
|
||||||
"subdomain": "",
|
|
||||||
"txt": ""}
|
|
||||||
|
|
||||||
// Valid data
|
|
||||||
updateJSON["subdomain"] = "a097455b-52cc-4569-90c8-7a4b97c6eba8"
|
|
||||||
updateJSON["txt"] = validTxtData
|
|
||||||
|
|
||||||
router, _, db := setupRouter(false, true)
|
|
||||||
server := httptest.NewServer(router)
|
|
||||||
defer server.Close()
|
|
||||||
e := getExpect(t, server)
|
|
||||||
oldDb := db.GetBackend()
|
|
||||||
mdb, mock, _ := sqlmock.New()
|
|
||||||
db.SetBackend(mdb)
|
|
||||||
defer db.Close()
|
|
||||||
mock.ExpectBegin()
|
|
||||||
mock.ExpectPrepare("UPDATE records").WillReturnError(errors.New("error"))
|
|
||||||
e.POST("/update").
|
|
||||||
WithJSON(updateJSON).
|
|
||||||
Expect().
|
|
||||||
Status(http.StatusInternalServerError).
|
|
||||||
JSON().Object().
|
|
||||||
ContainsKey("error")
|
|
||||||
db.SetBackend(oldDb)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestApiManyUpdateWithCredentials(t *testing.T) {
|
|
||||||
validTxtData := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
||||||
|
|
||||||
router, _, db := setupRouter(true, false)
|
|
||||||
server := httptest.NewServer(router)
|
|
||||||
defer server.Close()
|
|
||||||
e := getExpect(t, server)
|
|
||||||
// User without defined CIDR masks
|
|
||||||
newUser, err := db.Register(acmedns.Cidrslice{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Could not create new user, got error [%v]", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// User with defined allow from - CIDR masks, all invalid
|
|
||||||
// (httpexpect doesn't provide a way to mock remote ip)
|
|
||||||
newUserWithCIDR, err := db.Register(acmedns.Cidrslice{"192.168.1.1/32", "invalid"})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Could not create new user with CIDR, got error [%v]", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Another user with valid CIDR mask to match the httpexpect default
|
|
||||||
newUserWithValidCIDR, err := db.Register(acmedns.Cidrslice{"10.1.2.3/32", "invalid"})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Could not create new user with a valid CIDR, got error [%v]", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range []struct {
|
|
||||||
user string
|
|
||||||
pass string
|
|
||||||
subdomain string
|
|
||||||
txt interface{}
|
|
||||||
status int
|
|
||||||
}{
|
|
||||||
{"non-uuid-user", "tooshortpass", "non-uuid-subdomain", validTxtData, 401},
|
|
||||||
{"a097455b-52cc-4569-90c8-7a4b97c6eba8", "tooshortpass", "bb97455b-52cc-4569-90c8-7a4b97c6eba8", validTxtData, 401},
|
|
||||||
{"a097455b-52cc-4569-90c8-7a4b97c6eba8", "LongEnoughPassButNoUserExists___________", "bb97455b-52cc-4569-90c8-7a4b97c6eba8", validTxtData, 401},
|
|
||||||
{newUser.Username.String(), newUser.Password, "a097455b-52cc-4569-90c8-7a4b97c6eba8", validTxtData, 401},
|
|
||||||
{newUser.Username.String(), newUser.Password, newUser.Subdomain, "tooshortfortxt", 400},
|
|
||||||
{newUser.Username.String(), newUser.Password, newUser.Subdomain, 1234567890, 400},
|
|
||||||
{newUser.Username.String(), newUser.Password, newUser.Subdomain, validTxtData, 200},
|
|
||||||
{newUserWithCIDR.Username.String(), newUserWithCIDR.Password, newUserWithCIDR.Subdomain, validTxtData, 401},
|
|
||||||
{newUserWithValidCIDR.Username.String(), newUserWithValidCIDR.Password, newUserWithValidCIDR.Subdomain, validTxtData, 200},
|
|
||||||
{newUser.Username.String(), "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", newUser.Subdomain, validTxtData, 401},
|
|
||||||
} {
|
|
||||||
updateJSON := map[string]interface{}{
|
|
||||||
"subdomain": test.subdomain,
|
|
||||||
"txt": test.txt}
|
|
||||||
e.POST("/update").
|
|
||||||
WithJSON(updateJSON).
|
|
||||||
WithHeader("X-Api-User", test.user).
|
|
||||||
WithHeader("X-Api-Key", test.pass).
|
|
||||||
WithHeader("X-Forwarded-For", "10.1.2.3").
|
|
||||||
Expect().
|
|
||||||
Status(test.status)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestApiManyUpdateWithIpCheckHeaders(t *testing.T) {
|
|
||||||
|
|
||||||
router, adnsapi, db := setupRouter(false, false)
|
|
||||||
server := httptest.NewServer(router)
|
|
||||||
defer server.Close()
|
|
||||||
e := getExpect(t, server)
|
|
||||||
// Use header checks from default header (X-Forwarded-For)
|
|
||||||
adnsapi.Config.API.UseHeader = true
|
|
||||||
// User without defined CIDR masks
|
|
||||||
newUser, err := db.Register(acmedns.Cidrslice{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Could not create new user, got error [%v]", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
newUserWithCIDR, err := db.Register(acmedns.Cidrslice{"192.168.1.2/32", "invalid"})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Could not create new user with CIDR, got error [%v]", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
newUserWithIP6CIDR, err := db.Register(acmedns.Cidrslice{"2002:c0a8::0/32"})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Could not create a new user with IP6 CIDR, got error [%v]", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range []struct {
|
|
||||||
user acmedns.ACMETxt
|
|
||||||
headerValue string
|
|
||||||
status int
|
|
||||||
}{
|
|
||||||
{newUser, "whatever goes", 200},
|
|
||||||
{newUser, "10.0.0.1, 1.2.3.4 ,3.4.5.6", 200},
|
|
||||||
{newUserWithCIDR, "127.0.0.1", 401},
|
|
||||||
{newUserWithCIDR, "10.0.0.1, 10.0.0.2, 192.168.1.3", 401},
|
|
||||||
{newUserWithCIDR, "10.1.1.1 ,192.168.1.2, 8.8.8.8", 200},
|
|
||||||
{newUserWithIP6CIDR, "2002:c0a8:b4dc:0d3::0", 200},
|
|
||||||
{newUserWithIP6CIDR, "2002:c0a7:0ff::0", 401},
|
|
||||||
{newUserWithIP6CIDR, "2002:c0a8:d3ad:b33f:c0ff:33b4:dc0d:3b4d", 200},
|
|
||||||
} {
|
|
||||||
updateJSON := map[string]interface{}{
|
|
||||||
"subdomain": test.user.Subdomain,
|
|
||||||
"txt": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}
|
|
||||||
e.POST("/update").
|
|
||||||
WithJSON(updateJSON).
|
|
||||||
WithHeader("X-Api-User", test.user.Username.String()).
|
|
||||||
WithHeader("X-Api-Key", test.user.Password).
|
|
||||||
WithHeader("X-Forwarded-For", test.headerValue).
|
|
||||||
Expect().
|
|
||||||
Status(test.status)
|
|
||||||
}
|
|
||||||
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
103
pkg/api/auth.go
@ -1,103 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@ -1,76 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
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,361 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
_ "github.com/glebarez/go-sqlite"
|
|
||||||
_ "github.com/lib/pq"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
"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.
|
|
||||||
var DBVersion = 1
|
|
||||||
|
|
||||||
var acmeTable = `
|
|
||||||
CREATE TABLE IF NOT EXISTS acmedns(
|
|
||||||
Name TEXT,
|
|
||||||
Value TEXT
|
|
||||||
);`
|
|
||||||
|
|
||||||
var userTable = `
|
|
||||||
CREATE TABLE IF NOT EXISTS records(
|
|
||||||
Username TEXT UNIQUE NOT NULL PRIMARY KEY,
|
|
||||||
Password TEXT UNIQUE NOT NULL,
|
|
||||||
Subdomain TEXT UNIQUE NOT NULL,
|
|
||||||
AllowFrom TEXT
|
|
||||||
);`
|
|
||||||
|
|
||||||
var txtTable = `
|
|
||||||
CREATE TABLE IF NOT EXISTS txt(
|
|
||||||
Subdomain TEXT NOT NULL,
|
|
||||||
Value TEXT NOT NULL DEFAULT '',
|
|
||||||
LastUpdate INT
|
|
||||||
);`
|
|
||||||
|
|
||||||
var txtTablePG = `
|
|
||||||
CREATE TABLE IF NOT EXISTS txt(
|
|
||||||
rowid SERIAL,
|
|
||||||
Subdomain TEXT NOT NULL,
|
|
||||||
Value TEXT NOT NULL DEFAULT '',
|
|
||||||
LastUpdate INT
|
|
||||||
);`
|
|
||||||
|
|
||||||
// getSQLiteStmt replaces all PostgreSQL prepared statement placeholders (eg. $1, $2) with SQLite variant "?"
|
|
||||||
func getSQLiteStmt(s string) string {
|
|
||||||
re, _ := regexp.Compile(`\$[0-9]`)
|
|
||||||
return re.ReplaceAllString(s, "?")
|
|
||||||
}
|
|
||||||
|
|
||||||
func Init(config *acmedns.AcmeDnsConfig, logger *zap.SugaredLogger) (acmedns.AcmednsDB, error) {
|
|
||||||
var d = &acmednsdb{Config: config, Logger: logger}
|
|
||||||
d.Mutex.Lock()
|
|
||||||
defer d.Mutex.Unlock()
|
|
||||||
db, err := sql.Open(config.Database.Engine, config.Database.Connection)
|
|
||||||
if err != nil {
|
|
||||||
return d, err
|
|
||||||
}
|
|
||||||
d.DB = db
|
|
||||||
// Check version first to try to catch old versions without version string
|
|
||||||
var versionString string
|
|
||||||
_ = d.DB.QueryRow("SELECT Value FROM acmedns WHERE Name='db_version'").Scan(&versionString)
|
|
||||||
if versionString == "" {
|
|
||||||
versionString = "0"
|
|
||||||
}
|
|
||||||
_, _ = d.DB.Exec(acmeTable)
|
|
||||||
_, _ = d.DB.Exec(userTable)
|
|
||||||
if config.Database.Engine == "sqlite" {
|
|
||||||
_, _ = d.DB.Exec(txtTable)
|
|
||||||
} else {
|
|
||||||
_, _ = d.DB.Exec(txtTablePG)
|
|
||||||
}
|
|
||||||
// If everything is fine, handle db upgrade tasks
|
|
||||||
if err == nil {
|
|
||||||
err = d.checkDBUpgrades(versionString)
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
if versionString == "0" {
|
|
||||||
// No errors so we should now be in version 1
|
|
||||||
insversion := fmt.Sprintf("INSERT INTO acmedns (Name, Value) values('db_version', '%d')", DBVersion)
|
|
||||||
_, err = db.Exec(insversion)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return d, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *acmednsdb) checkDBUpgrades(versionString string) error {
|
|
||||||
var err error
|
|
||||||
version, err := strconv.Atoi(versionString)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if version != DBVersion {
|
|
||||||
return d.handleDBUpgrades(version)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *acmednsdb) handleDBUpgrades(version int) error {
|
|
||||||
if version == 0 {
|
|
||||||
return d.handleDBUpgradeTo1()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *acmednsdb) handleDBUpgradeTo1() error {
|
|
||||||
var err error
|
|
||||||
var subdomains []string
|
|
||||||
rows, err := d.DB.Query("SELECT Subdomain FROM records")
|
|
||||||
if err != nil {
|
|
||||||
d.Logger.Errorw("Error in DB upgrade",
|
|
||||||
"error", err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
for rows.Next() {
|
|
||||||
var subdomain string
|
|
||||||
err = rows.Scan(&subdomain)
|
|
||||||
if err != nil {
|
|
||||||
d.Logger.Errorw("Error in DB upgrade while reading values",
|
|
||||||
"error", err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
subdomains = append(subdomains, subdomain)
|
|
||||||
}
|
|
||||||
err = rows.Err()
|
|
||||||
if err != nil {
|
|
||||||
d.Logger.Errorw("Error in DB upgrade while inserting values",
|
|
||||||
"error", err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
tx, err := d.DB.Begin()
|
|
||||||
// Rollback if errored, commit if not
|
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
_ = tx.Rollback()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_ = tx.Commit()
|
|
||||||
}()
|
|
||||||
_, _ = tx.Exec("DELETE FROM txt")
|
|
||||||
for _, subdomain := range subdomains {
|
|
||||||
if subdomain != "" {
|
|
||||||
// Insert two rows for each subdomain to txt table
|
|
||||||
err = d.NewTXTValuesInTransaction(tx, subdomain)
|
|
||||||
if err != nil {
|
|
||||||
d.Logger.Errorw("Error in DB upgrade while inserting values",
|
|
||||||
"error", err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// SQLite doesn't support dropping columns
|
|
||||||
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 LastActive")
|
|
||||||
}
|
|
||||||
_, err = tx.Exec("UPDATE acmedns SET Value='1' WHERE Name='db_version'")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTXTValuesInTransaction creates two rows for subdomain to the txt table
|
|
||||||
func (d *acmednsdb) NewTXTValuesInTransaction(tx *sql.Tx, subdomain string) error {
|
|
||||||
var err error
|
|
||||||
instr := fmt.Sprintf("INSERT INTO txt (Subdomain, LastUpdate) values('%s', 0)", subdomain)
|
|
||||||
_, _ = tx.Exec(instr)
|
|
||||||
_, _ = tx.Exec(instr)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *acmednsdb) Register(afrom acmedns.Cidrslice) (acmedns.ACMETxt, error) {
|
|
||||||
d.Mutex.Lock()
|
|
||||||
defer d.Mutex.Unlock()
|
|
||||||
var err error
|
|
||||||
tx, err := d.DB.Begin()
|
|
||||||
// Rollback if errored, commit if not
|
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
_ = tx.Rollback()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_ = tx.Commit()
|
|
||||||
}()
|
|
||||||
a := acmedns.NewACMETxt()
|
|
||||||
a.AllowFrom = acmedns.Cidrslice(afrom.ValidEntries())
|
|
||||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(a.Password), 10)
|
|
||||||
regSQL := `
|
|
||||||
INSERT INTO records(
|
|
||||||
Username,
|
|
||||||
Password,
|
|
||||||
Subdomain,
|
|
||||||
AllowFrom)
|
|
||||||
values($1, $2, $3, $4)`
|
|
||||||
if d.Config.Database.Engine == "sqlite" {
|
|
||||||
regSQL = getSQLiteStmt(regSQL)
|
|
||||||
}
|
|
||||||
sm, err := tx.Prepare(regSQL)
|
|
||||||
if err != nil {
|
|
||||||
d.Logger.Errorw("Database error in prepare",
|
|
||||||
"error", err.Error())
|
|
||||||
return a, fmt.Errorf("failed to prepare registration statement: %w", err)
|
|
||||||
}
|
|
||||||
defer sm.Close()
|
|
||||||
_, err = sm.Exec(a.Username.String(), passwordHash, a.Subdomain, a.AllowFrom.JSON())
|
|
||||||
if err == nil {
|
|
||||||
err = d.NewTXTValuesInTransaction(tx, a.Subdomain)
|
|
||||||
}
|
|
||||||
return a, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *acmednsdb) GetByUsername(u uuid.UUID) (acmedns.ACMETxt, error) {
|
|
||||||
d.Mutex.Lock()
|
|
||||||
defer d.Mutex.Unlock()
|
|
||||||
var results []acmedns.ACMETxt
|
|
||||||
getSQL := `
|
|
||||||
SELECT Username, Password, Subdomain, AllowFrom
|
|
||||||
FROM records
|
|
||||||
WHERE Username=$1 LIMIT 1
|
|
||||||
`
|
|
||||||
if d.Config.Database.Engine == "sqlite" {
|
|
||||||
getSQL = getSQLiteStmt(getSQL)
|
|
||||||
}
|
|
||||||
|
|
||||||
sm, err := d.DB.Prepare(getSQL)
|
|
||||||
if err != nil {
|
|
||||||
return acmedns.ACMETxt{}, err
|
|
||||||
}
|
|
||||||
defer sm.Close()
|
|
||||||
rows, err := sm.Query(u.String())
|
|
||||||
if err != nil {
|
|
||||||
return acmedns.ACMETxt{}, fmt.Errorf("failed to query user: %w", err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
// It will only be one row though
|
|
||||||
for rows.Next() {
|
|
||||||
txt, err := d.getModelFromRow(rows)
|
|
||||||
if err != nil {
|
|
||||||
return acmedns.ACMETxt{}, err
|
|
||||||
}
|
|
||||||
results = append(results, txt)
|
|
||||||
}
|
|
||||||
if len(results) > 0 {
|
|
||||||
return results[0], nil
|
|
||||||
}
|
|
||||||
return acmedns.ACMETxt{}, fmt.Errorf("user not found: %s", u.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *acmednsdb) GetTXTForDomain(domain string) ([]string, error) {
|
|
||||||
d.Mutex.Lock()
|
|
||||||
defer d.Mutex.Unlock()
|
|
||||||
domain = acmedns.SanitizeString(domain)
|
|
||||||
var txts []string
|
|
||||||
getSQL := `
|
|
||||||
SELECT Value FROM txt WHERE Subdomain=$1 LIMIT 2
|
|
||||||
`
|
|
||||||
if d.Config.Database.Engine == "sqlite" {
|
|
||||||
getSQL = getSQLiteStmt(getSQL)
|
|
||||||
}
|
|
||||||
|
|
||||||
sm, err := d.DB.Prepare(getSQL)
|
|
||||||
if err != nil {
|
|
||||||
return txts, err
|
|
||||||
}
|
|
||||||
defer sm.Close()
|
|
||||||
rows, err := sm.Query(domain)
|
|
||||||
if err != nil {
|
|
||||||
return txts, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
var rtxt string
|
|
||||||
err = rows.Scan(&rtxt)
|
|
||||||
if err != nil {
|
|
||||||
return txts, err
|
|
||||||
}
|
|
||||||
txts = append(txts, rtxt)
|
|
||||||
}
|
|
||||||
return txts, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *acmednsdb) Update(a acmedns.ACMETxtPost) error {
|
|
||||||
d.Mutex.Lock()
|
|
||||||
defer d.Mutex.Unlock()
|
|
||||||
var err error
|
|
||||||
// Data in a is already sanitized
|
|
||||||
timenow := time.Now().Unix()
|
|
||||||
|
|
||||||
updSQL := `
|
|
||||||
UPDATE txt SET Value=$1, LastUpdate=$2
|
|
||||||
WHERE rowid=(
|
|
||||||
SELECT rowid FROM txt WHERE Subdomain=$3 ORDER BY LastUpdate LIMIT 1)
|
|
||||||
`
|
|
||||||
if d.Config.Database.Engine == "sqlite" {
|
|
||||||
updSQL = getSQLiteStmt(updSQL)
|
|
||||||
}
|
|
||||||
|
|
||||||
sm, err := d.DB.Prepare(updSQL)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer sm.Close()
|
|
||||||
_, err = sm.Exec(a.Value, timenow, a.Subdomain)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *acmednsdb) getModelFromRow(r *sql.Rows) (acmedns.ACMETxt, error) {
|
|
||||||
txt := acmedns.ACMETxt{}
|
|
||||||
afrom := ""
|
|
||||||
err := r.Scan(
|
|
||||||
&txt.Username,
|
|
||||||
&txt.Password,
|
|
||||||
&txt.Subdomain,
|
|
||||||
&afrom)
|
|
||||||
if err != nil {
|
|
||||||
d.Logger.Errorw("Row scan error",
|
|
||||||
"error", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
cslice := acmedns.Cidrslice{}
|
|
||||||
err = json.Unmarshal([]byte(afrom), &cslice)
|
|
||||||
if err != nil {
|
|
||||||
d.Logger.Errorw("JSON unmarshall error",
|
|
||||||
"error", err.Error())
|
|
||||||
}
|
|
||||||
txt.AllowFrom = cslice
|
|
||||||
return txt, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *acmednsdb) Close() {
|
|
||||||
d.DB.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *acmednsdb) GetBackend() *sql.DB {
|
|
||||||
return d.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *acmednsdb) SetBackend(backend *sql.DB) {
|
|
||||||
d.DB = backend
|
|
||||||
}
|
|
||||||
@ -1,396 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,160 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,150 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,106 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# go test doesn't play well with noexec /tmp
|
|
||||||
sudo mkdir /gotmp
|
|
||||||
sudo mount tmpfs -t tmpfs /gotmp
|
|
||||||
TMPDIR=/gotmp go test -v -race
|
|
||||||
sudo umount /gotmp
|
|
||||||
sudo rm -rf /gotmp
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
# 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`.
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
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
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
[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"
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
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:
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
requests
|
|
||||||
dnspython
|
|
||||||
@ -1,105 +0,0 @@
|
|||||||
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)
|
|
||||||
87
types.go
Normal file
87
types.go
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"github.com/satori/go.uuid"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DNSConf is global configuration struct
|
||||||
|
var DNSConf 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"`
|
||||||
|
Port string
|
||||||
|
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)
|
||||||
|
GetByDomain(string) ([]ACMETxt, error)
|
||||||
|
Update(ACMETxt) error
|
||||||
|
GetBackend() *sql.DB
|
||||||
|
SetBackend(*sql.DB)
|
||||||
|
Close()
|
||||||
|
Lock()
|
||||||
|
Unlock()
|
||||||
|
}
|
||||||
@ -1,9 +0,0 @@
|
|||||||
//go:build !windows
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import "syscall"
|
|
||||||
|
|
||||||
func setUmask() {
|
|
||||||
syscall.Umask(0077)
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
//go:build windows
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
func setUmask() {
|
|
||||||
// umask is not supported on Windows
|
|
||||||
}
|
|
||||||
82
util.go
Normal file
82
util.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"math/big"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
log "github.com/Sirupsen/logrus"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
Normal file
97
util_test.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
49
validation.go
Normal file
49
validation.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@ -1,15 +1,12 @@
|
|||||||
package api
|
package main
|
||||||
|
|
||||||
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.Parse("a097455b-52cc-4569-90c8-7a4b97c6eba8")
|
v1, _ := uuid.FromString("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
|
||||||
@ -57,9 +54,7 @@ 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", true},
|
{"a-97455b-52cc-4569-90c8-7a4b97c6eba8", false},
|
||||||
{"foo.example.com", false},
|
|
||||||
{"foo-example-com", true},
|
|
||||||
{"", false},
|
{"", false},
|
||||||
{"&!#!25123!%!'%", false},
|
{"&!#!25123!%!'%", false},
|
||||||
} {
|
} {
|
||||||
@ -105,7 +100,7 @@ func TestCorrectPassword(t *testing.T) {
|
|||||||
false},
|
false},
|
||||||
{"", "", false},
|
{"", "", false},
|
||||||
} {
|
} {
|
||||||
ret := acmedns.CorrectPassword(test.pw, test.hash)
|
ret := 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)
|
||||||
}
|
}
|
||||||
@ -114,12 +109,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 acmedns.Cidrslice
|
input cidrslice
|
||||||
output acmedns.Cidrslice
|
output cidrslice
|
||||||
}{
|
}{
|
||||||
{acmedns.Cidrslice{"10.0.0.1/24"}, acmedns.Cidrslice{"10.0.0.1/24"}},
|
{cidrslice{"10.0.0.1/24"}, cidrslice{"10.0.0.1/24"}},
|
||||||
{acmedns.Cidrslice{"invalid", "127.0.0.1/32"}, acmedns.Cidrslice{"127.0.0.1/32"}},
|
{cidrslice{"invalid", "127.0.0.1/32"}, cidrslice{"127.0.0.1/32"}},
|
||||||
{acmedns.Cidrslice{"2002:c0a8::0/32", "8.8.8.8/32"}, acmedns.Cidrslice{"2002:c0a8::0/32", "8.8.8.8/32"}},
|
{cidrslice{"2002:c0a8::0/32", "8.8.8.8/32"}, 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) {
|
||||||
549
vendor/vendor.json
vendored
Normal file
549
vendor/vendor.json
vendored
Normal file
@ -0,0 +1,549 @@
|
|||||||
|
{
|
||||||
|
"comment": "",
|
||||||
|
"ignore": "test",
|
||||||
|
"package": [
|
||||||
|
{
|
||||||
|
"checksumSHA1": "hqDDDpue/5363luidNMBS8z8eJU=",
|
||||||
|
"path": "github.com/BurntSushi/toml",
|
||||||
|
"revision": "99064174e013895bbd9b025c31100bd1d9b590ca",
|
||||||
|
"revisionTime": "2016-07-17T15:07:09Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "jRtYpPa7CRuA+LP4ELF9c9CjJao=",
|
||||||
|
"path": "github.com/Sirupsen/logrus",
|
||||||
|
"revision": "a437dfd2463eaedbec3dfe443e477d3b0a810b3f",
|
||||||
|
"revisionTime": "2016-11-18T19:45:39Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "Lglgc8iIRhqbqd8fpAZKpo/eqeY=",
|
||||||
|
"path": "github.com/Sirupsen/logrus/hooks/test",
|
||||||
|
"revision": "a437dfd2463eaedbec3dfe443e477d3b0a810b3f",
|
||||||
|
"revisionTime": "2016-11-18T19:45:39Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "kMfAFLobZymMrCOm/Xi/g9gnJOU=",
|
||||||
|
"path": "github.com/ajg/form",
|
||||||
|
"revision": "523a5da1a92f01b01f840b61689c0340a0243532",
|
||||||
|
"revisionTime": "2016-08-22T23:00:20Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "OFu4xJEIjiI8Suu+j/gabfp+y6Q=",
|
||||||
|
"origin": "github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew",
|
||||||
|
"path": "github.com/davecgh/go-spew/spew",
|
||||||
|
"revision": "18a02ba4a312f95da08ff4cfc0055750ce50ae9e",
|
||||||
|
"revisionTime": "2016-11-17T07:43:51Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "0xIBiVOmW6JxXyxOZsBTtHF1Jxw=",
|
||||||
|
"path": "github.com/erikstmartin/go-testdb",
|
||||||
|
"revision": "8d10e4a1bae52cd8b81ffdec3445890d6dccab3d",
|
||||||
|
"revisionTime": "2016-02-19T21:45:06Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "KCWVxG+J8SxHGlGiUghe0KBGsa8=",
|
||||||
|
"path": "github.com/fatih/structs",
|
||||||
|
"revision": "dc3312cb1a4513a366c4c9e622ad55c32df12ed3",
|
||||||
|
"revisionTime": "2016-08-07T23:55:29Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "bnOeTmDN6UfzLWaifmbPnAH2yWs=",
|
||||||
|
"path": "github.com/gavv/gojsondiff",
|
||||||
|
"revision": "36046c6e558e7f854ebd3fd97d1e9812ebe8709b",
|
||||||
|
"revisionTime": "2016-05-10T20:49:56Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "GJ1YuqzOYzEBDcO8wE2Jv4xihLI=",
|
||||||
|
"path": "github.com/gavv/gojsondiff/formatter",
|
||||||
|
"revision": "36046c6e558e7f854ebd3fd97d1e9812ebe8709b",
|
||||||
|
"revisionTime": "2016-05-10T20:49:56Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "5B8ZLx876nOQv4dChpvamEEjHMs=",
|
||||||
|
"path": "github.com/gavv/httpexpect",
|
||||||
|
"revision": "35d8329d8ee24194c2103dfa7cd1c715be3bced2",
|
||||||
|
"revisionTime": "2016-11-16T16:40:02Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "4HpMp8lo5lc64CIb3pULsFlr4ms=",
|
||||||
|
"path": "github.com/gavv/monotime",
|
||||||
|
"revision": "47d58efa69556a936a3c15eb2ed42706d968ab01",
|
||||||
|
"revisionTime": "2016-10-10T19:08:48Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "gpccqXvJy99CBDrHS+m4BDZprvk=",
|
||||||
|
"path": "github.com/geekypanda/httpcache",
|
||||||
|
"revision": "76ba6c68462ae362cda7564c44492b95322b363a",
|
||||||
|
"revisionTime": "2016-11-19T13:53:50Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "V9dSQUcmEVqwUazrRx8RB6XwTdk=",
|
||||||
|
"path": "github.com/geekypanda/httpcache/internal",
|
||||||
|
"revision": "76ba6c68462ae362cda7564c44492b95322b363a",
|
||||||
|
"revisionTime": "2016-11-19T13:53:50Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "AauUe5dA6Ex6d4wCI88Tpl72kE8=",
|
||||||
|
"path": "github.com/geekypanda/httpcache/internal/fhttp",
|
||||||
|
"revision": "76ba6c68462ae362cda7564c44492b95322b363a",
|
||||||
|
"revisionTime": "2016-11-19T13:53:50Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "wSO3uLsYdlhjq+mXJsw1FYRhrhU=",
|
||||||
|
"path": "github.com/geekypanda/httpcache/internal/fhttp/rule",
|
||||||
|
"revision": "76ba6c68462ae362cda7564c44492b95322b363a",
|
||||||
|
"revisionTime": "2016-11-19T13:53:50Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "XvHvSUy+R57XJTGV7Q8SoAuXpd4=",
|
||||||
|
"path": "github.com/geekypanda/httpcache/internal/nethttp",
|
||||||
|
"revision": "76ba6c68462ae362cda7564c44492b95322b363a",
|
||||||
|
"revisionTime": "2016-11-19T13:53:50Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "y84oxzFPj8hrrVEh3m6rnx9WpYA=",
|
||||||
|
"path": "github.com/geekypanda/httpcache/internal/nethttp/rule",
|
||||||
|
"revision": "76ba6c68462ae362cda7564c44492b95322b363a",
|
||||||
|
"revisionTime": "2016-11-19T13:53:50Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "G3LMqGx0ztSCcFB9SX7K01owtvY=",
|
||||||
|
"path": "github.com/geekypanda/httpcache/internal/server",
|
||||||
|
"revision": "76ba6c68462ae362cda7564c44492b95322b363a",
|
||||||
|
"revisionTime": "2016-11-19T13:53:50Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "25qSuESQLAwpJKpK8+Ne81GtQ40=",
|
||||||
|
"origin": "github.com/kataras/go-fs/vendor/github.com/google/go-github/github",
|
||||||
|
"path": "github.com/google/go-github/github",
|
||||||
|
"revision": "c029e113d9faaf558b730f06041c8bf9545a3502",
|
||||||
|
"revisionTime": "2016-10-31T04:20:56Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "yyAzHoiVLu+xywYI2BDyRq6sOqE=",
|
||||||
|
"path": "github.com/google/go-querystring/query",
|
||||||
|
"revision": "9235644dd9e52eeae6fa48efd539fdc351a0af53",
|
||||||
|
"revisionTime": "2016-03-11T01:20:12Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "XjzE8S3JcN+F48Tmv6ZAf7kwqKU=",
|
||||||
|
"origin": "github.com/kataras/go-websocket/vendor/github.com/gorilla/websocket",
|
||||||
|
"path": "github.com/gorilla/websocket",
|
||||||
|
"revision": "188e6bbd55486e22f0ddc3f013105c518548fbbb",
|
||||||
|
"revisionTime": "2016-11-04T23:40:48Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "poYpUe2RyFrWeBoTAdB6eM4F+eM=",
|
||||||
|
"origin": "github.com/kataras/go-fs/vendor/github.com/hashicorp/go-version",
|
||||||
|
"path": "github.com/hashicorp/go-version",
|
||||||
|
"revision": "c029e113d9faaf558b730f06041c8bf9545a3502",
|
||||||
|
"revisionTime": "2016-10-31T04:20:56Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "hwGdeQbcfc2RvIQS5wAaYRKJDd4=",
|
||||||
|
"path": "github.com/imdario/mergo",
|
||||||
|
"revision": "50d4dbd4eb0e84778abe37cefef140271d96fade",
|
||||||
|
"revisionTime": "2016-05-17T06:44:35Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "XFHQ1CK3YYzMx9M/C4HSygSav6c=",
|
||||||
|
"path": "github.com/imkira/go-interpol",
|
||||||
|
"revision": "5accad8134979a6ac504d456a6c7f1c53da237ca",
|
||||||
|
"revisionTime": "2016-09-18T18:34:49Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "Snx6GCbPUzXgc8J40CjQMvu2dFE=",
|
||||||
|
"path": "github.com/iris-contrib/formBinder",
|
||||||
|
"revision": "023b47796b500a9a9407e81cbf1cf5ebf45718e0",
|
||||||
|
"revisionTime": "2016-10-31T05:12:53Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "i6IqjmScYfsN+3oZ+Vt+SO6kghw=",
|
||||||
|
"path": "github.com/iris-contrib/lego/acme",
|
||||||
|
"revision": "095d7f6459c501cb15319aa2754afa221b81a3ec",
|
||||||
|
"revisionTime": "2016-10-22T05:37:38Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "tiu4UWUWrJctQNnfz/dRFog0ksI=",
|
||||||
|
"path": "github.com/iris-contrib/letsencrypt",
|
||||||
|
"revision": "1a3e5c619a13b307df3b1b4da7cb7e57d2e156dd",
|
||||||
|
"revisionTime": "2016-10-21T19:44:08Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "56wyOoLznFBSCqliBRjiwKAs0R8=",
|
||||||
|
"path": "github.com/iris-contrib/middleware/cors",
|
||||||
|
"revision": "fd204bbe1fe40fb92800f5dfbb5d637776a30b46",
|
||||||
|
"revisionTime": "2016-10-31T04:52:57Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "nGy5c2Euaeu0gEU0nxqFb6jO5Rw=",
|
||||||
|
"path": "github.com/iris-contrib/websocket",
|
||||||
|
"revision": "cc9f1712095295a828e9a2efaef388d30b9c7760",
|
||||||
|
"revisionTime": "2016-10-09T18:06:29Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "oOOoWMOCyOoZ594DKzopz9w9kew=",
|
||||||
|
"path": "github.com/kataras/go-errors",
|
||||||
|
"revision": "0f977b82cc78d5d31bb75fb6f903ad9e852c8bbd",
|
||||||
|
"revisionTime": "2016-09-18T10:12:19Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "oxrjhEMJaD/MqQwo3xHE8QA9Tfk=",
|
||||||
|
"path": "github.com/kataras/go-fs",
|
||||||
|
"revision": "c029e113d9faaf558b730f06041c8bf9545a3502",
|
||||||
|
"revisionTime": "2016-10-31T04:20:56Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "WQ2UlASRdzSwbYYwUKUyadUxFx8=",
|
||||||
|
"path": "github.com/kataras/go-options",
|
||||||
|
"revision": "23b556c1b935c594ec6d71ff81ead4dbeec3aa8d",
|
||||||
|
"revisionTime": "2016-09-09T04:20:19Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "xs0wwYHPqJTz0NBzH9tajb+tDqU=",
|
||||||
|
"path": "github.com/kataras/go-serializer",
|
||||||
|
"revision": "0bd874a15c70db74ef2e668e5eeda27041f03b81",
|
||||||
|
"revisionTime": "2016-10-31T04:11:45Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "L2YxcGSPjpnO6V+fT/Cx1JU1nB4=",
|
||||||
|
"path": "github.com/kataras/go-serializer/data",
|
||||||
|
"revision": "0bd874a15c70db74ef2e668e5eeda27041f03b81",
|
||||||
|
"revisionTime": "2016-10-31T04:11:45Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "sDz+RpxfMabDdSgU3hISAofwKlE=",
|
||||||
|
"path": "github.com/kataras/go-serializer/json",
|
||||||
|
"revision": "0bd874a15c70db74ef2e668e5eeda27041f03b81",
|
||||||
|
"revisionTime": "2016-10-31T04:11:45Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "ACZvyU6FytObgwOB6UhPgNlVTAE=",
|
||||||
|
"path": "github.com/kataras/go-serializer/jsonp",
|
||||||
|
"revision": "0bd874a15c70db74ef2e668e5eeda27041f03b81",
|
||||||
|
"revisionTime": "2016-10-31T04:11:45Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "7IyA1DftN+yYPQxppxaA7cUOeRM=",
|
||||||
|
"path": "github.com/kataras/go-serializer/markdown",
|
||||||
|
"revision": "0bd874a15c70db74ef2e668e5eeda27041f03b81",
|
||||||
|
"revisionTime": "2016-10-31T04:11:45Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "ffDcrYR6cOsfl3Sbu5lnE+3SkP4=",
|
||||||
|
"path": "github.com/kataras/go-serializer/text",
|
||||||
|
"revision": "0bd874a15c70db74ef2e668e5eeda27041f03b81",
|
||||||
|
"revisionTime": "2016-10-31T04:11:45Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "vqhmBFZ37nWG1jxPpvxynW1bwrE=",
|
||||||
|
"path": "github.com/kataras/go-serializer/xml",
|
||||||
|
"revision": "0bd874a15c70db74ef2e668e5eeda27041f03b81",
|
||||||
|
"revisionTime": "2016-10-31T04:11:45Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "arCdUcupgxsKcfbzE3XLhYPu4B8=",
|
||||||
|
"path": "github.com/kataras/go-sessions",
|
||||||
|
"revision": "5fbb60d99b3cd100a2ae586cb49474368cebab58",
|
||||||
|
"revisionTime": "2016-11-06T05:58:01Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "llGXIznKrKh9Xog3E8UW5HUGwx4=",
|
||||||
|
"path": "github.com/kataras/go-template",
|
||||||
|
"revision": "457f21178102f4688603eccbb4f2e8d5ae1023bf",
|
||||||
|
"revisionTime": "2016-11-11T10:06:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "D+rA4C4aTWlXRhROhIwsMXcWqsM=",
|
||||||
|
"path": "github.com/kataras/go-template/html",
|
||||||
|
"revision": "457f21178102f4688603eccbb4f2e8d5ae1023bf",
|
||||||
|
"revisionTime": "2016-11-11T10:06:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "rMMwNiM+ovdbJi+pqt23Pv5e6W8=",
|
||||||
|
"path": "github.com/kataras/go-websocket",
|
||||||
|
"revision": "188e6bbd55486e22f0ddc3f013105c518548fbbb",
|
||||||
|
"revisionTime": "2016-11-04T23:40:48Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "TfPCJRr/ogxz1mH5+6BiCj6sl0w=",
|
||||||
|
"path": "github.com/kataras/iris",
|
||||||
|
"revision": "290a9cad3dab65f3eb1bbab3ef9a252bb59da74c",
|
||||||
|
"revisionTime": "2016-11-23T20:46:19Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "8xYLTnyqaix1rdjB0EEeSTe14Wg=",
|
||||||
|
"path": "github.com/kataras/iris/httptest",
|
||||||
|
"revision": "290a9cad3dab65f3eb1bbab3ef9a252bb59da74c",
|
||||||
|
"revisionTime": "2016-11-23T20:46:19Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "RrW2mq7rcdH2cK/3oizmdTipEK4=",
|
||||||
|
"path": "github.com/kataras/iris/utils",
|
||||||
|
"revision": "290a9cad3dab65f3eb1bbab3ef9a252bb59da74c",
|
||||||
|
"revisionTime": "2016-11-23T20:46:19Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "vfzz7zTL9TZLpFO7NC1H6/Du3+s=",
|
||||||
|
"path": "github.com/klauspost/compress/flate",
|
||||||
|
"revision": "e3b7981a12dd3cab49afa1d3a50e715846f23732",
|
||||||
|
"revisionTime": "2016-11-06T14:34:36Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "V1lQwkoDR1fPmZBSgkmZjgZofeU=",
|
||||||
|
"path": "github.com/klauspost/compress/gzip",
|
||||||
|
"revision": "e3b7981a12dd3cab49afa1d3a50e715846f23732",
|
||||||
|
"revisionTime": "2016-11-06T14:34:36Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "+azPXaZpPF14YHRghNAer13ThQU=",
|
||||||
|
"path": "github.com/klauspost/compress/zlib",
|
||||||
|
"revision": "e3b7981a12dd3cab49afa1d3a50e715846f23732",
|
||||||
|
"revisionTime": "2016-11-06T14:34:36Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "iKPMvbAueGfdyHcWCgzwKzm8WVo=",
|
||||||
|
"path": "github.com/klauspost/cpuid",
|
||||||
|
"revision": "09cded8978dc9e80714c4d85b0322337b0a1e5e0",
|
||||||
|
"revisionTime": "2016-03-02T07:53:16Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "BM6ZlNJmtKy3GBoWwg2X55gnZ4A=",
|
||||||
|
"path": "github.com/klauspost/crc32",
|
||||||
|
"revision": "cb6bfca970f6908083f26f39a79009d608efd5cd",
|
||||||
|
"revisionTime": "2016-10-16T15:41:25Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "avqi4lkviHdrNJ92cXCwrw9x870=",
|
||||||
|
"path": "github.com/lib/pq",
|
||||||
|
"revision": "d8eeeb8bae8896dd8e1b7e514ab0d396c4f12a1b",
|
||||||
|
"revisionTime": "2016-11-03T02:43:54Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "xppHi82MLqVx1eyQmbhTesAEjx8=",
|
||||||
|
"path": "github.com/lib/pq/oid",
|
||||||
|
"revision": "d8eeeb8bae8896dd8e1b7e514ab0d396c4f12a1b",
|
||||||
|
"revisionTime": "2016-11-03T02:43:54Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "gQPNnwneFBYZXKVN0PaKrqiGemA=",
|
||||||
|
"path": "github.com/mattn/go-sqlite3",
|
||||||
|
"revision": "fba66eb11643069e747022997e9be3b502b2c6fb",
|
||||||
|
"revisionTime": "2016-11-11T16:58:19Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "z2i7dm7KC0aicOx2PLcHRv6NibU=",
|
||||||
|
"origin": "github.com/kataras/go-serializer/vendor/github.com/microcosm-cc/bluemonday",
|
||||||
|
"path": "github.com/microcosm-cc/bluemonday",
|
||||||
|
"revision": "0bd874a15c70db74ef2e668e5eeda27041f03b81",
|
||||||
|
"revisionTime": "2016-10-31T04:11:45Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "VZrdtf1OtAeYaHwL1opfi08HwnM=",
|
||||||
|
"path": "github.com/miekg/dns",
|
||||||
|
"revision": "271c58e0c14f552178ea321a545ff9af38930f39",
|
||||||
|
"revisionTime": "2016-11-22T06:12:14Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "CxNwJP++vjUAyy3bbJnNss1Il9Q=",
|
||||||
|
"path": "github.com/moul/http2curl",
|
||||||
|
"revision": "4e24498b31dba4683efb9d35c1c8a91e2eda28c8",
|
||||||
|
"revisionTime": "2016-10-31T19:45:48Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "zKKp5SZ3d3ycKe4EKMNT0BqAWBw=",
|
||||||
|
"origin": "github.com/stretchr/testify/vendor/github.com/pmezard/go-difflib/difflib",
|
||||||
|
"path": "github.com/pmezard/go-difflib/difflib",
|
||||||
|
"revision": "18a02ba4a312f95da08ff4cfc0055750ce50ae9e",
|
||||||
|
"revisionTime": "2016-11-17T07:43:51Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "41hlerAYPe6EFKtgmK/AEf5xBP4=",
|
||||||
|
"origin": "github.com/kataras/go-serializer/vendor/github.com/russross/blackfriday",
|
||||||
|
"path": "github.com/russross/blackfriday",
|
||||||
|
"revision": "0bd874a15c70db74ef2e668e5eeda27041f03b81",
|
||||||
|
"revisionTime": "2016-10-31T04:11:45Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "zmC8/3V4ls53DJlNTKDZwPSC/dA=",
|
||||||
|
"path": "github.com/satori/go.uuid",
|
||||||
|
"revision": "b061729afc07e77a8aa4fad0a2fd840958f1942a",
|
||||||
|
"revisionTime": "2016-09-27T10:08:44Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "4RKtyBgrsGEZwtiypp6uq6139MQ=",
|
||||||
|
"path": "github.com/sergi/go-diff/diffmatchpatch",
|
||||||
|
"revision": "552b4e9bbdca9e5adafd95ee98c822fdd11b330b",
|
||||||
|
"revisionTime": "2016-11-02T18:40:45Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "kbgJvKG3NRoqU91rYnXGnyR+8HQ=",
|
||||||
|
"path": "github.com/shurcooL/sanitized_anchor_name",
|
||||||
|
"revision": "1dba4b3954bc059efc3991ec364f9f9a35f597d2",
|
||||||
|
"revisionTime": "2016-09-18T04:11:01Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "fOuTjfiFhmBY4iJJXquzV4ojBy8=",
|
||||||
|
"origin": "github.com/iris-contrib/lego/vendor/github.com/square/go-jose",
|
||||||
|
"path": "github.com/square/go-jose",
|
||||||
|
"revision": "095d7f6459c501cb15319aa2754afa221b81a3ec",
|
||||||
|
"revisionTime": "2016-10-22T05:37:38Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "hIEmcd7hIDqO/xWSp1rJJHd0TpE=",
|
||||||
|
"path": "github.com/stretchr/testify/assert",
|
||||||
|
"revision": "18a02ba4a312f95da08ff4cfc0055750ce50ae9e",
|
||||||
|
"revisionTime": "2016-11-17T07:43:51Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "omdvCNu8sJIc9FbOfObC484M7Dg=",
|
||||||
|
"path": "github.com/stretchr/testify/require",
|
||||||
|
"revision": "18a02ba4a312f95da08ff4cfc0055750ce50ae9e",
|
||||||
|
"revisionTime": "2016-11-17T07:43:51Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "LTOa3BADhwvT0wFCknPueQALm8I=",
|
||||||
|
"path": "github.com/valyala/bytebufferpool",
|
||||||
|
"revision": "e746df99fe4a3986f4d4f79e13c1e0117ce9c2f7",
|
||||||
|
"revisionTime": "2016-08-17T18:16:52Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "hLWrEWJTTxuiI6/L71Jt20truqI=",
|
||||||
|
"path": "github.com/valyala/fasthttp",
|
||||||
|
"revision": "1c39678a4dd0122de1b9a7e14e49b3e99b7d60b9",
|
||||||
|
"revisionTime": "2016-11-28T09:50:28Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "1j/ERUJk+d/UwnmA+oMUsrPxdSw=",
|
||||||
|
"path": "github.com/valyala/fasthttp/fasthttpadaptor",
|
||||||
|
"revision": "1c39678a4dd0122de1b9a7e14e49b3e99b7d60b9",
|
||||||
|
"revisionTime": "2016-11-28T09:50:28Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "nMWLZCTKLciURGG8o/KeEPUExkY=",
|
||||||
|
"path": "github.com/valyala/fasthttp/fasthttputil",
|
||||||
|
"revision": "1c39678a4dd0122de1b9a7e14e49b3e99b7d60b9",
|
||||||
|
"revisionTime": "2016-11-28T09:50:28Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "8qIEFviyMSKhh3e2vWdZFC6TNu4=",
|
||||||
|
"path": "github.com/valyala/fasthttp/stackless",
|
||||||
|
"revision": "1c39678a4dd0122de1b9a7e14e49b3e99b7d60b9",
|
||||||
|
"revisionTime": "2016-11-28T09:50:28Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "drSl/ipSHSsHWWTrp3WZw4LN/No=",
|
||||||
|
"path": "github.com/xeipuuv/gojsonpointer",
|
||||||
|
"revision": "e0fe6f68307607d540ed8eac07a342c33fa1b54a",
|
||||||
|
"revisionTime": "2015-10-27T08:21:46Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "pSoUW+qY6LwIJ5lFwGohPU5HUpg=",
|
||||||
|
"path": "github.com/xeipuuv/gojsonreference",
|
||||||
|
"revision": "e02fc20de94c78484cd5ffb007f8af96be030a45",
|
||||||
|
"revisionTime": "2015-08-08T06:50:54Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "vLmkhv7RXt4uOoS564cBIMzLT88=",
|
||||||
|
"path": "github.com/xeipuuv/gojsonschema",
|
||||||
|
"revision": "e18f0065e8c148fcf567ac43a3f8f5b66ac0720b",
|
||||||
|
"revisionTime": "2016-11-19T18:01:51Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "LmYXonZ72xAk0VmZB52DD+TTAOo=",
|
||||||
|
"path": "github.com/yalp/jsonpath",
|
||||||
|
"revision": "31a79c7593bb93eb10b163650d4a3e6ca190e4dc",
|
||||||
|
"revisionTime": "2015-08-12T00:39:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "OCkp7qxxdxjpoM3T6Q3CTiMP5kM=",
|
||||||
|
"path": "github.com/yudai/golcs",
|
||||||
|
"revision": "d1c525dea8ce39ea9a783d33cf08932305373f2c",
|
||||||
|
"revisionTime": "2015-04-05T16:34:35Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "TK1Yr8BbwionaaAvM+77lwAAx/8=",
|
||||||
|
"path": "golang.org/x/crypto/acme",
|
||||||
|
"revision": "ede567c8e044a5913dad1d1af3696d9da953104c",
|
||||||
|
"revisionTime": "2016-11-04T19:41:44Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "0gEWevUuowrpoQgcLSG76u+y8Uw=",
|
||||||
|
"path": "golang.org/x/crypto/acme/autocert",
|
||||||
|
"revision": "ede567c8e044a5913dad1d1af3696d9da953104c",
|
||||||
|
"revisionTime": "2016-11-04T19:41:44Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "vE43s37+4CJ2CDU6TlOUOYE0K9c=",
|
||||||
|
"path": "golang.org/x/crypto/bcrypt",
|
||||||
|
"revision": "ede567c8e044a5913dad1d1af3696d9da953104c",
|
||||||
|
"revisionTime": "2016-11-04T19:41:44Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "JsJdKXhz87gWenMwBeejTOeNE7k=",
|
||||||
|
"path": "golang.org/x/crypto/blowfish",
|
||||||
|
"revision": "ede567c8e044a5913dad1d1af3696d9da953104c",
|
||||||
|
"revisionTime": "2016-11-04T19:41:44Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "TJmmMKEHkGrmn+/39c9HiPpSQ3Q=",
|
||||||
|
"path": "golang.org/x/crypto/ocsp",
|
||||||
|
"revision": "ede567c8e044a5913dad1d1af3696d9da953104c",
|
||||||
|
"revisionTime": "2016-11-04T19:41:44Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "9jjO5GjLa0XF/nfWihF02RoH4qc=",
|
||||||
|
"path": "golang.org/x/net/context",
|
||||||
|
"revision": "4971afdc2f162e82d185353533d3cf16188a9f4e",
|
||||||
|
"revisionTime": "2016-11-15T21:05:04Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "WHc3uByvGaMcnSoI21fhzYgbOgg=",
|
||||||
|
"path": "golang.org/x/net/context/ctxhttp",
|
||||||
|
"revision": "4971afdc2f162e82d185353533d3cf16188a9f4e",
|
||||||
|
"revisionTime": "2016-11-15T21:05:04Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "vqc3a+oTUGX8PmD0TS+qQ7gmN8I=",
|
||||||
|
"path": "golang.org/x/net/html",
|
||||||
|
"revision": "4971afdc2f162e82d185353533d3cf16188a9f4e",
|
||||||
|
"revisionTime": "2016-11-15T21:05:04Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "00eQaGynDYrv3tL+C7l9xH0IDZg=",
|
||||||
|
"path": "golang.org/x/net/html/atom",
|
||||||
|
"revision": "4971afdc2f162e82d185353533d3cf16188a9f4e",
|
||||||
|
"revisionTime": "2016-11-15T21:05:04Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "AmZIW67T/HUlTTflTmOIy6jdq74=",
|
||||||
|
"path": "golang.org/x/net/publicsuffix",
|
||||||
|
"revision": "4971afdc2f162e82d185353533d3cf16188a9f4e",
|
||||||
|
"revisionTime": "2016-11-15T21:05:04Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "eFQDEix/mGnhwnFu/Hq63zMfrX8=",
|
||||||
|
"path": "golang.org/x/time/rate",
|
||||||
|
"revision": "f51c12702a4d776e4c1fa9b0fabab841babae631",
|
||||||
|
"revisionTime": "2016-10-28T04:02:39Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "MeXzn+OFdrU9/TGeMVz0GsRX+dM=",
|
||||||
|
"path": "gopkg.in/DATA-DOG/go-sqlmock.v1",
|
||||||
|
"revision": "d4cd2ca2ad1cc2130bba385aab072218f131f636",
|
||||||
|
"revisionTime": "2016-11-02T12:49:59Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "vSlztt3rfYwwDDKEiqUDWXl2LGw=",
|
||||||
|
"path": "gopkg.in/square/go-jose.v1/cipher",
|
||||||
|
"revision": "aa2e30fdd1fe9dd3394119af66451ae790d50e0d",
|
||||||
|
"revisionTime": "2016-09-23T00:08:11Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "UYvcpB3og7YJHbRu4feZFxXAU/A=",
|
||||||
|
"path": "gopkg.in/square/go-jose.v1/json",
|
||||||
|
"revision": "aa2e30fdd1fe9dd3394119af66451ae790d50e0d",
|
||||||
|
"revisionTime": "2016-09-23T00:08:11Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rootPath": "acme-dns"
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user