Compare commits
119
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdb1535fa7 | ||
|
|
68bb6ab654 | ||
|
|
835fbb9ef6 | ||
|
|
9c6ca258e1 | ||
|
|
d573f771a8 | ||
|
|
035a219f9f | ||
|
|
19069f50ec | ||
|
|
5de21916a3 | ||
|
|
3d5a512d1e | ||
|
|
9f7a158367 | ||
|
|
1681de11d2 | ||
|
|
88d3be685e | ||
|
|
aa3e7e1b75 | ||
|
|
af542b44a9 | ||
|
|
5b1e51534f | ||
|
|
637c82f407 | ||
|
|
e1f1d6af34 | ||
|
|
c13035a344 | ||
|
|
af5d2561d2 | ||
|
|
395cb7a62c | ||
|
|
5d74ff1142 | ||
|
|
37db83e5b7 | ||
|
|
41a1cff0ae | ||
|
|
4f5fad0d32 | ||
|
|
5651772837 | ||
|
|
09dc25d336 | ||
|
|
a88ee29755 | ||
|
|
de7fe3cb1d | ||
|
|
7a2f9f06b1 | ||
|
|
d695f72963 | ||
|
|
aff13a02fb | ||
|
|
7fbb5261c8 | ||
|
|
2da94f1462 | ||
|
|
ea4d125663 | ||
|
|
f64de0353d | ||
|
|
20411b650f | ||
|
|
3bb130d055 | ||
|
|
1d85bf75b0 | ||
|
|
c2c5c5cd70 | ||
|
|
a09073da12 | ||
|
|
4bb5e265ab | ||
|
|
b452d504c8 | ||
|
|
f76790426d | ||
|
|
f650e47fe5 | ||
|
|
dc0dd43017 | ||
|
|
c827ee4801 | ||
|
|
db2a6bc288 | ||
|
|
d66ccffaf6 | ||
|
|
eeff02e63b | ||
|
|
90ae6ee268 | ||
|
|
0fc5a8e848 | ||
|
|
ec013c0f25 | ||
|
|
856cc05881 | ||
|
|
75d4a30c1f | ||
|
|
8aa869b2f8 | ||
|
|
52e977ce1d | ||
|
|
0bbbf5ed39 | ||
|
|
fbd5c65a6a | ||
|
|
4646e7f7e5 | ||
|
|
3006cb712b | ||
|
|
fde566fe67 | ||
|
|
a17792d33c | ||
|
|
d1af1d029f | ||
|
|
a5b417901c | ||
|
|
065083781e | ||
|
|
0af5dc2cd9 | ||
|
|
1c918fcaa4 | ||
|
|
299c520c2b | ||
|
|
3343d943d6 | ||
|
|
7744357b61 | ||
|
|
14f552e970 | ||
|
|
f2d1fc692b | ||
|
|
dd12fa7c9b | ||
|
|
0991b3e3c9 | ||
|
|
d18cd65374 | ||
|
|
8eed75b495 | ||
|
|
46b9c45e1b | ||
|
|
ed82e15349 | ||
|
|
7b2203ccca | ||
|
|
945013579f | ||
|
|
3ee50d0c20 | ||
|
|
f2fe21934c | ||
|
|
07aa5e0043 | ||
|
|
01f010e35e | ||
|
|
cd3a587e43 | ||
|
|
439da9c09f | ||
|
|
5c2e60a828 | ||
|
|
d542ee03b5 | ||
|
|
92f8cc2802 | ||
|
|
978ac5d62b | ||
|
|
4d214d7f52 | ||
|
|
f463d07d0b | ||
|
|
9a908d7d6b | ||
|
|
11c852ee91 | ||
|
|
32608e9f47 | ||
|
|
77b5fda6fb | ||
|
|
efdd560ee4 | ||
|
|
830cceb62c | ||
|
|
9f896759f0 | ||
|
|
476f6fc555 | ||
|
|
e2e84ede6b | ||
|
|
562d7cbad4 | ||
|
|
5470ba7a41 | ||
|
|
665455d319 | ||
|
|
c5337fc841 | ||
|
|
733245fb3d | ||
|
|
ba695134ce | ||
|
|
b0cd264c71 | ||
|
|
02d42bff30 | ||
|
|
fd9ce4606d | ||
|
|
93871a7cec | ||
|
|
9c54da3ee6 | ||
|
|
9c639223ce | ||
|
|
c70a6cffb0 | ||
|
|
0ec12dbc5f | ||
|
|
41b2ff5940 | ||
|
|
2bfeedda4c | ||
|
|
872e2b7c6f | ||
|
|
220ef6d3c0 |
@@ -0,0 +1,33 @@
|
||||
name: Go
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
schedule:
|
||||
# Run every 12 hours, at the 15 minute mark. E.g.
|
||||
# 2020-11-29 00:15:00 UTC, 2020-11-29 12:15:00 UTC, 2020-11-30 00:15:00 UTC
|
||||
- cron: '15 */12 * * *'
|
||||
jobs:
|
||||
|
||||
build:
|
||||
name: Build and Unit Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ^1.13
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./...
|
||||
|
||||
- name: Test
|
||||
run: go test -v -race -covermode=atomic -coverprofile=coverage.out ./...
|
||||
|
||||
- name: Upload Coverage
|
||||
uses: shogo82148/actions-goveralls@v1
|
||||
with:
|
||||
path-to-profile: coverage.out
|
||||
@@ -0,0 +1,25 @@
|
||||
name: golangci-lint
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
schedule:
|
||||
# Run every 12 hours, at the 15 minute mark. E.g.
|
||||
# 2020-11-29 00:15:00 UTC, 2020-11-29 12:15:00 UTC, 2020-11-30 00:15:00 UTC
|
||||
- cron: '15 */12 * * *'
|
||||
jobs:
|
||||
golangci:
|
||||
name: Lint Sourcecode
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
with:
|
||||
version: v1.35
|
||||
+1
-2
@@ -3,5 +3,4 @@ acme-dns.db
|
||||
acme-dns.log
|
||||
.vagrant
|
||||
coverage.out
|
||||
vendor/*/
|
||||
/vendor/**/.git
|
||||
.idea/
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
builds:
|
||||
- binary: acme-dns
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- 386
|
||||
- amd64
|
||||
|
||||
archive:
|
||||
format: tar.gz
|
||||
files:
|
||||
- LICENSE
|
||||
- README.md
|
||||
- Dockerfile
|
||||
- config.cfg
|
||||
- acme-dns.service
|
||||
|
||||
sign:
|
||||
artifacts: checksum
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
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
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
FROM golang:alpine AS builder
|
||||
LABEL maintainer="joona@kuori.org"
|
||||
|
||||
RUN apk add --update gcc musl-dev git
|
||||
|
||||
ENV GOPATH /tmp/buildcache
|
||||
RUN git clone https://github.com/joohoi/acme-dns /tmp/acme-dns
|
||||
WORKDIR /tmp/acme-dns
|
||||
RUN CGO_ENABLED=1 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
|
||||
@@ -8,9 +8,11 @@ A simplified DNS server with a RESTful HTTP API to provide a simple way to autom
|
||||
Many DNS servers do not provide an API to enable automation for the ACME DNS challenges. Those which do, give the keys way too much power.
|
||||
Leaving the keys laying around your random boxes is too often a requirement to have a meaningful process automation.
|
||||
|
||||
Acme-dns provides a simple API exclusively for TXT record updates and should be used with ACME magic "\_acme-challenge" - subdomain CNAME records. This way, in the unfortunate exposure of API keys, the effetcs are limited to the subdomain TXT record in question.
|
||||
Acme-dns provides a simple API exclusively for TXT record updates and should be used with ACME magic "\_acme-challenge" - subdomain CNAME records. This way, in the unfortunate exposure of API keys, the effects are limited to the subdomain TXT record in question.
|
||||
|
||||
So basically it boils down to **accessibility** and **security**
|
||||
So basically it boils down to **accessibility** and **security**.
|
||||
|
||||
For longer explanation of the underlying issue and other proposed solutions, see a blog post on the topic from EFF deeplinks blog: https://www.eff.org/deeplinks/2018/02/technical-deep-dive-securing-automation-acme-dns-challenge-validation
|
||||
|
||||
## Features
|
||||
- Simplified DNS server, serving your ACME DNS challenges (TXT)
|
||||
@@ -18,16 +20,20 @@ So basically it boils down to **accessibility** and **security**
|
||||
- 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
|
||||
- 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)
|
||||
|
||||
## 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)
|
||||
|
||||
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):
|
||||
Using acme-dns is a three-step process (provided you already have the self-hosted server set up):
|
||||
|
||||
- Get credentials and unique subdomain (simple POST request to eg. https://auth.acme-dns.io/register)
|
||||
- Create a (ACME magic) CNAME record to your existing zone, pointing to the subdomain you got from the registration. (eg. `_acme-challenge.domainiwantcertfor.tld. CNAME a097455b-52cc-4569-90c8-7a4b97c6eba8.auth.example.org` )
|
||||
- Use your credentials to POST a new DNS challenge values to an acme-dns server for the CA to validate them off of.
|
||||
- Use your credentials to POST new DNS challenge values to an acme-dns server for the CA to validate from.
|
||||
- Crontab and forget.
|
||||
|
||||
## API
|
||||
@@ -36,9 +42,9 @@ Using acme-dns is a three-step process (provided you already have the self-hoste
|
||||
|
||||
The method returns a new unique subdomain and credentials needed to update your record.
|
||||
Fulldomain is where you can point your own `_acme-challenge` subdomain CNAME record to.
|
||||
With the credentials, you can update the TXT response in the service to match the challenge token, later referred as \_\_\_validation\_token\_recieved\_from\_the\_ca\_\_\_, given out by the Certificate Authority.
|
||||
With the credentials, you can update the TXT response in the service to match the challenge token, later referred as \_\_\_validation\_token\_received\_from\_the\_ca\_\_\_, given out by the Certificate Authority.
|
||||
|
||||
**Optional:**: You can POST JSON data to limit the /update requests to predefined source networks using CIDR notation.
|
||||
**Optional:**: You can POST JSON data to limit the `/update` requests to predefined source networks using CIDR notation.
|
||||
|
||||
```POST /register```
|
||||
|
||||
@@ -48,7 +54,8 @@ With the credentials, you can update the TXT response in the service to match th
|
||||
"allowfrom": [
|
||||
"192.168.100.1/24",
|
||||
"1.2.3.4/32",
|
||||
"2002:c0a8:2a00::0/40",
|
||||
"2002:c0a8:2a00::0/40"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -58,7 +65,8 @@ With the credentials, you can update the TXT response in the service to match th
|
||||
{
|
||||
"allowfrom": [
|
||||
"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",
|
||||
"password": "htB9mR9DYgcu9bX_afHF62erXaH2TS7bg9KW3F7Z",
|
||||
@@ -76,14 +84,14 @@ The method allows you to update the TXT answer contents of your unique subdomain
|
||||
#### Required headers
|
||||
| Header name | Description | Example |
|
||||
| ------------- |--------------------------------------------|-------------------------------------------------------|
|
||||
| X-Api-User | UUIDv4 username recieved from registration | `X-Api-User: c36f50e8-4632-44f0-83fe-e070fef28a10` |
|
||||
| X-Api-Key | Password recieved from registration | `X-Api-Key: htB9mR9DYgcu9bX_afHF62erXaH2TS7bg9KW3F7Z` |
|
||||
| X-Api-User | UUIDv4 username received from registration | `X-Api-User: c36f50e8-4632-44f0-83fe-e070fef28a10` |
|
||||
| X-Api-Key | Password received from registration | `X-Api-Key: htB9mR9DYgcu9bX_afHF62erXaH2TS7bg9KW3F7Z` |
|
||||
|
||||
#### Example input
|
||||
```json
|
||||
{
|
||||
"subdomain": "8e5700ea-a4bf-41c7-8a77-e990661dcc6a",
|
||||
"txt": "___validation_token_recieved_from_the_ca___",
|
||||
"txt": "___validation_token_received_from_the_ca___"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -92,63 +100,154 @@ The method allows you to update the TXT answer contents of your unique subdomain
|
||||
```Status: 200 OK```
|
||||
```json
|
||||
{
|
||||
"txt": "___validation_token_recieved_from_the_ca___",
|
||||
"txt": "___validation_token_received_from_the_ca___"
|
||||
}
|
||||
```
|
||||
|
||||
### Health check endpoint
|
||||
|
||||
The method can be used to check readiness and/or liveness of the server. It will return status code 200 on success or won't be reachable.
|
||||
|
||||
```GET /health```
|
||||
|
||||
## Self-hosted
|
||||
|
||||
You are encouraged to run your own acme-dns instance, because you are effectively authorizing the acme-dns server to act on your behalf in providing the answer to challengeing CA, making the instance able to request (and get issued) a TLS certificate for the domain that has CNAME pointing to it.
|
||||
You are encouraged to run your own acme-dns instance, because you are effectively authorizing the acme-dns server to act on your behalf in providing the answer to the challenging CA, making the instance able to request (and get issued) a TLS certificate for the domain that has CNAME pointing to it.
|
||||
|
||||
Check out how in the INSTALL section.
|
||||
|
||||
## 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```
|
||||
See the INSTALL section for information on how to do this.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
1) Install [Go](https://golang.org/doc/install)
|
||||
1) Install [Go 1.13 or newer](https://golang.org/doc/install).
|
||||
|
||||
2) Clone this repo: `git clone https://github.com/joohoi/acme-dns $GOPATH/src/acme-dns`
|
||||
2) Build acme-dns:
|
||||
```
|
||||
git clone https://github.com/joohoi/acme-dns
|
||||
cd acme-dns
|
||||
export GOPATH=/tmp/acme-dns
|
||||
go build
|
||||
```
|
||||
|
||||
3) Install govendor. ‘go get -u github.com/kardianos/govendor’ . This is used for dependency handling.
|
||||
3) Move the built acme-dns binary to a directory in your $PATH, for example:
|
||||
`sudo mv acme-dns /usr/local/bin`
|
||||
|
||||
4) Get dependencies: `cd $GOPATH/src/acme-dns` and `govendor sync`
|
||||
4) Edit config.cfg to suit your needs (see [configuration](#configuration)). `acme-dns` will read the configuration file from `/etc/acme-dns/config.cfg` or `./config.cfg`, or a location specified with the `-c` flag.
|
||||
|
||||
5) Build ACME-DNS: `go build`
|
||||
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.
|
||||
|
||||
6) Edit config.cfg to suit your needs (see [configuration](#configuration))
|
||||
1) Make sure that you have moved the configuration file to `/etc/acme-dns/config.cfg` so that acme-dns can access it globally.
|
||||
|
||||
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.
|
||||
2) Move the acme-dns executable from `~/go/bin/acme-dns` to `/usr/local/bin/acme-dns` (Any location will work, just be sure to change `acme-dns.service` to match).
|
||||
|
||||
3) Create a minimal acme-dns user: `sudo adduser --system --gecos "acme-dns Service" --disabled-password --group --home /var/lib/acme-dns acme-dns`.
|
||||
|
||||
4) Move the systemd service unit from `acme-dns.service` to `/etc/systemd/system/acme-dns.service`.
|
||||
|
||||
5) Reload systemd units: `sudo systemctl daemon-reload`.
|
||||
|
||||
6) Enable acme-dns on boot: `sudo systemctl enable acme-dns.service`.
|
||||
|
||||
7) Run acme-dns: `sudo systemctl start acme-dns.service`.
|
||||
|
||||
6) If you did not install the systemd service, run `acme-dns`. Please note that acme-dns needs to open a privileged port (53, domain), so it needs to be run with elevated privileges.
|
||||
|
||||
### Using Docker
|
||||
|
||||
1) Pull the latest acme-dns Docker image: `docker pull joohoi/acme-dns`.
|
||||
|
||||
2) Create directories: `config` for the configuration file, and `data` for the sqlite3 database.
|
||||
|
||||
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
|
||||
|
||||
```bash
|
||||
[general]
|
||||
# dns interface
|
||||
listen = ":53"
|
||||
# protocol, "udp", "udp4", "udp6" or "tcp", "tcp4", "tcp6"
|
||||
protocol = "udp"
|
||||
# domain name to serve the requests off of
|
||||
# DNS interface. Note that systemd-resolved may reserve port 53 on 127.0.0.53
|
||||
# In this case acme-dns will error out and you will need to define the listening interface
|
||||
# for example: listen = "127.0.0.1:53"
|
||||
listen = "127.0.0.1:53"
|
||||
# protocol, "both", "both4", "both6", "udp", "udp4", "udp6" or "tcp", "tcp4", "tcp6"
|
||||
protocol = "both"
|
||||
# domain name to serve the requests off of
|
||||
domain = "auth.example.org"
|
||||
# zone name server
|
||||
nsname = "ns1.auth.example.org"
|
||||
# zone name server
|
||||
nsname = "auth.example.org"
|
||||
# admin email address, where @ is substituted with .
|
||||
nsadmin = "admin.example.org"
|
||||
# predefined records served in addition to the TXT
|
||||
records = [
|
||||
# default A
|
||||
"auth.example.org. A 192.168.1.100",
|
||||
# A
|
||||
"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.",
|
||||
# domain pointing to the public IP of your acme-dns server
|
||||
"auth.example.org. A 198.51.100.1",
|
||||
# specify that auth.example.org will resolve any *.auth.example.org records
|
||||
"auth.example.org. NS auth.example.org.",
|
||||
]
|
||||
# debug messages from CORS etc
|
||||
debug = false
|
||||
@@ -157,23 +256,34 @@ debug = false
|
||||
# Database engine to use, sqlite3 or postgres
|
||||
engine = "sqlite3"
|
||||
# Connection string, filename for sqlite3 and postgres://$username:$password@$host/$db_name for postgres
|
||||
connection = "acme-dns.db"
|
||||
# Please note that the default Docker image uses path /var/lib/acme-dns/acme-dns.db for sqlite3
|
||||
connection = "/var/lib/acme-dns/acme-dns.db"
|
||||
# connection = "postgres://user:password@localhost/acmedns_db"
|
||||
|
||||
[api]
|
||||
# domain name to listen requests for, mandatory if using tls = "letsencrypt"
|
||||
api_domain = ""
|
||||
# listen ip eg. 127.0.0.1
|
||||
ip = "0.0.0.0"
|
||||
# disable registration endpoint
|
||||
disable_registration = false
|
||||
# listen port, eg. 443 for default HTTPS
|
||||
port = "8080"
|
||||
# possible values: "letsencrypt", "cert", "none"
|
||||
tls = "none"
|
||||
port = "443"
|
||||
# possible values: "letsencrypt", "letsencryptstaging", "cert", "none"
|
||||
tls = "letsencryptstaging"
|
||||
# only used if tls = "cert"
|
||||
tls_cert_privkey = "/etc/tls/example.org/privkey.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
|
||||
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]
|
||||
# logging level: "error", "warning", "info" or "debug"
|
||||
@@ -182,26 +292,108 @@ loglevel = "debug"
|
||||
logtype = "stdout"
|
||||
# file path for logfile TODO
|
||||
# logfile = "./acme-dns.log"
|
||||
# format, either "json" or "text"
|
||||
# format, either "json" or "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
|
||||
|
||||
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)
|
||||
|
||||
|
||||
## Changelog
|
||||
|
||||
- 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
|
||||
|
||||
## TODO
|
||||
|
||||
- Logging to a file
|
||||
- DNSSEC
|
||||
- Want to see something implemented, make a feature request!
|
||||
|
||||
## Contributing
|
||||
|
||||
acme-dns is open for contributions.
|
||||
If you have an improvement, please open a Pull Request.
|
||||
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!
|
||||
|
||||
## License
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -1,7 +1,7 @@
|
||||
# -*- mode: ruby -*-
|
||||
# vi: set ft=ruby :
|
||||
|
||||
# Vagratnfile for running integration tests with PostgreSQL
|
||||
# Vagrantfile for running integration tests with PostgreSQL
|
||||
|
||||
VAGRANTFILE_API_VERSION = "2"
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=Limited DNS server with RESTful HTTP API to handle ACME DNS challenges easily and securely
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=acme-dns
|
||||
Group=acme-dns
|
||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
WorkingDirectory=~
|
||||
ExecStart=/usr/local/bin/acme-dns
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
+22
-7
@@ -4,7 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
|
||||
"github.com/satori/go.uuid"
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ACMETxt is the default structure for the user controlled record
|
||||
@@ -12,8 +13,7 @@ type ACMETxt struct {
|
||||
Username uuid.UUID
|
||||
Password string
|
||||
ACMETxtPost
|
||||
LastActive int64
|
||||
AllowFrom cidrslice
|
||||
AllowFrom cidrslice
|
||||
}
|
||||
|
||||
// ACMETxtPost holds the DNS part of the ACMETxt struct
|
||||
@@ -30,12 +30,22 @@ func (c *cidrslice) JSON() string {
|
||||
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(v)
|
||||
_, _, err := net.ParseCIDR(sanitizeIPv6addr(v))
|
||||
if err == nil {
|
||||
valid = append(valid, v)
|
||||
valid = append(valid, sanitizeIPv6addr(v))
|
||||
}
|
||||
}
|
||||
return valid
|
||||
@@ -48,6 +58,7 @@ func (a ACMETxt) allowedFrom(ip string) bool {
|
||||
if len(a.AllowFrom.ValidEntries()) == 0 {
|
||||
return true
|
||||
}
|
||||
log.WithFields(log.Fields{"ip": remoteIP}).Debug("Checking if update is permitted from IP")
|
||||
for _, v := range a.AllowFrom.ValidEntries() {
|
||||
_, vnet, _ := net.ParseCIDR(v)
|
||||
if vnet.Contains(remoteIP) {
|
||||
@@ -60,6 +71,10 @@ func (a ACMETxt) allowedFrom(ip string) bool {
|
||||
// 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
|
||||
@@ -71,8 +86,8 @@ func (a ACMETxt) allowedFromList(ips []string) bool {
|
||||
func newACMETxt() ACMETxt {
|
||||
var a = ACMETxt{}
|
||||
password := generatePassword(40)
|
||||
a.Username = uuid.NewV4()
|
||||
a.Username = uuid.New()
|
||||
a.Password = password
|
||||
a.Subdomain = uuid.NewV4().String()
|
||||
a.Subdomain = uuid.New().String()
|
||||
return a
|
||||
}
|
||||
|
||||
@@ -1,108 +1,113 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/kataras/iris"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// 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"})
|
||||
// RegResponse is a struct for registration response JSON
|
||||
type RegResponse struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Fulldomain string `json:"fulldomain"`
|
||||
Subdomain string `json:"subdomain"`
|
||||
Allowfrom []string `json:"allowfrom"`
|
||||
}
|
||||
|
||||
func webRegisterPost(ctx *iris.Context) {
|
||||
var regJSON iris.Map
|
||||
func webRegisterPost(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
var regStatus int
|
||||
var reg []byte
|
||||
var err error
|
||||
aTXT := ACMETxt{}
|
||||
_ = ctx.ReadJSON(&aTXT)
|
||||
bdata, _ := ioutil.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 := DB.Register(aTXT.AllowFrom)
|
||||
if err != nil {
|
||||
errstr := fmt.Sprintf("%v", err)
|
||||
regJSON = iris.Map{"error": errstr}
|
||||
regStatus = iris.StatusInternalServerError
|
||||
reg = jsonError(errstr)
|
||||
regStatus = http.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")
|
||||
regStruct := RegResponse{nu.Username.String(), nu.Password, nu.Subdomain + "." + Config.General.Domain, nu.Subdomain, nu.AllowFrom.ValidEntries()}
|
||||
regStatus = http.StatusCreated
|
||||
reg, err = json.Marshal(regStruct)
|
||||
if err != nil {
|
||||
regStatus = http.StatusInternalServerError
|
||||
reg = jsonError("json_error")
|
||||
log.WithFields(log.Fields{"error": "json"}).Debug("Could not marshal JSON")
|
||||
}
|
||||
}
|
||||
ctx.JSON(regStatus, regJSON)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(regStatus)
|
||||
_, _ = w.Write(reg)
|
||||
}
|
||||
|
||||
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)
|
||||
func webUpdatePost(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
var updStatus int
|
||||
var upd []byte
|
||||
// Get user
|
||||
a, ok := r.Context().Value(ACMETxtKey).(ACMETxt)
|
||||
if !ok {
|
||||
log.WithFields(log.Fields{"error": "context"}).Error("Context error")
|
||||
}
|
||||
// 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(a.Subdomain) {
|
||||
log.WithFields(log.Fields{"error": "subdomain", "subdomain": a.Subdomain, "txt": a.Value}).Debug("Bad update data")
|
||||
updStatus = http.StatusBadRequest
|
||||
upd = jsonError("bad_subdomain")
|
||||
} else if !validTXT(a.Value) {
|
||||
log.WithFields(log.Fields{"error": "txt", "subdomain": a.Subdomain, "txt": a.Value}).Debug("Bad update data")
|
||||
updStatus = http.StatusBadRequest
|
||||
upd = jsonError("bad_txt")
|
||||
} else if validSubdomain(a.Subdomain) && validTXT(a.Value) {
|
||||
err := DB.Update(a.ACMETxtPost)
|
||||
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
|
||||
updStatus = http.StatusInternalServerError
|
||||
upd = jsonError("db_error")
|
||||
} else {
|
||||
log.WithFields(log.Fields{"subdomain": a.Subdomain, "txt": a.Value}).Debug("TXT updated")
|
||||
updStatus = http.StatusOK
|
||||
upd = []byte("{\"txt\": \"" + a.Value + "\"}")
|
||||
}
|
||||
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
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(updStatus)
|
||||
_, _ = w.Write(upd)
|
||||
}
|
||||
|
||||
func webUpdatePostError(ctx *iris.Context, err error, status int) {
|
||||
errStr := fmt.Sprintf("%v", err)
|
||||
updJSON := iris.Map{"error": errStr}
|
||||
ctx.JSON(status, updJSON)
|
||||
// Endpoint used to check the readiness and/or liveness (health) of the server.
|
||||
func healthCheck(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
+225
-47
@@ -1,16 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/gavv/httpexpect"
|
||||
"github.com/kataras/iris"
|
||||
"github.com/kataras/iris/httptest"
|
||||
"gopkg.in/DATA-DOG/go-sqlmock.v1"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/gavv/httpexpect"
|
||||
"github.com/google/uuid"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/rs/cors"
|
||||
)
|
||||
|
||||
func setupIris(t *testing.T, debug bool, noauth bool) *httpexpect.Expect {
|
||||
iris.ResetDefault()
|
||||
// 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 := 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 {
|
||||
api := httprouter.New()
|
||||
var dbcfg = dbsettings{
|
||||
Engine: "sqlite3",
|
||||
Connection: ":memory:"}
|
||||
@@ -19,30 +56,37 @@ func setupIris(t *testing.T, debug bool, noauth bool) *httpexpect.Expect {
|
||||
Port: "8080",
|
||||
TLS: "none",
|
||||
CorsOrigins: []string{"*"},
|
||||
UseHeader: false,
|
||||
UseHeader: true,
|
||||
HeaderName: "X-Forwarded-For",
|
||||
}
|
||||
var dnscfg = DNSConfig{
|
||||
API: httpapicfg,
|
||||
Database: dbcfg,
|
||||
}
|
||||
DNSConf = dnscfg
|
||||
var ForceAuth = authMiddleware{}
|
||||
iris.Post("/register", webRegisterPost)
|
||||
Config = dnscfg
|
||||
c := cors.New(cors.Options{
|
||||
AllowedOrigins: Config.API.CorsOrigins,
|
||||
AllowedMethods: []string{"GET", "POST"},
|
||||
OptionsPassthrough: false,
|
||||
Debug: Config.General.Debug,
|
||||
})
|
||||
api.POST("/register", webRegisterPost)
|
||||
api.GET("/health", healthCheck)
|
||||
if noauth {
|
||||
iris.Post("/update", webUpdatePost)
|
||||
api.POST("/update", noAuth(webUpdatePost))
|
||||
} else {
|
||||
iris.Post("/update", ForceAuth.Serve, webUpdatePost)
|
||||
api.POST("/update", Auth(webUpdatePost))
|
||||
}
|
||||
httptestcfg := httptest.DefaultConfiguration()
|
||||
httptestcfg.Debug = debug
|
||||
return httptest.New(iris.Default, t, httptestcfg)
|
||||
return c.Handler(api)
|
||||
}
|
||||
|
||||
func TestApiRegister(t *testing.T) {
|
||||
e := setupIris(t, false, false)
|
||||
router := setupRouter(false, false)
|
||||
server := httptest.NewServer(router)
|
||||
defer server.Close()
|
||||
e := getExpect(t, server)
|
||||
e.POST("/register").Expect().
|
||||
Status(iris.StatusCreated).
|
||||
Status(http.StatusCreated).
|
||||
JSON().Object().
|
||||
ContainsKey("fulldomain").
|
||||
ContainsKey("subdomain").
|
||||
@@ -52,14 +96,15 @@ func TestApiRegister(t *testing.T) {
|
||||
|
||||
allowfrom := map[string][]interface{}{
|
||||
"allowfrom": []interface{}{"123.123.123.123/32",
|
||||
"1010.10.10.10/24",
|
||||
"invalid"},
|
||||
"2001:db8:a0b:12f0::1/32",
|
||||
"[::1]/64",
|
||||
},
|
||||
}
|
||||
|
||||
response := e.POST("/register").
|
||||
WithJSON(allowfrom).
|
||||
Expect().
|
||||
Status(iris.StatusCreated).
|
||||
Status(http.StatusCreated).
|
||||
JSON().Object().
|
||||
ContainsKey("fulldomain").
|
||||
ContainsKey("subdomain").
|
||||
@@ -68,11 +113,71 @@ func TestApiRegister(t *testing.T) {
|
||||
ContainsKey("allowfrom").
|
||||
NotContainsKey("error")
|
||||
|
||||
response.Value("allowfrom").Array().Elements("123.123.123.123/32")
|
||||
response.Value("allowfrom").Array().Elements("123.123.123.123/32", "2001:db8:a0b:12f0::1/32", "::1/64")
|
||||
}
|
||||
|
||||
func TestApiRegisterBadAllowFrom(t *testing.T) {
|
||||
router := setupRouter(false, false)
|
||||
server := httptest.NewServer(router)
|
||||
defer server.Close()
|
||||
e := getExpect(t, server)
|
||||
invalidVals := []string{
|
||||
"invalid",
|
||||
"1.2.3.4/33",
|
||||
"1.2/24",
|
||||
"1.2.3.4",
|
||||
"12345:db8:a0b:12f0::1/32",
|
||||
"1234::123::123::1/32",
|
||||
}
|
||||
|
||||
for _, v := range invalidVals {
|
||||
|
||||
allowfrom := map[string][]interface{}{
|
||||
"allowfrom": []interface{}{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) {
|
||||
e := setupIris(t, false, false)
|
||||
router := setupRouter(false, false)
|
||||
server := httptest.NewServer(router)
|
||||
defer server.Close()
|
||||
e := getExpect(t, server)
|
||||
oldDb := DB.GetBackend()
|
||||
db, mock, _ := sqlmock.New()
|
||||
DB.SetBackend(db)
|
||||
@@ -80,16 +185,79 @@ func TestApiRegisterWithMockDB(t *testing.T) {
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectPrepare("INSERT INTO records").WillReturnError(errors.New("error"))
|
||||
e.POST("/register").Expect().
|
||||
Status(iris.StatusInternalServerError).
|
||||
Status(http.StatusInternalServerError).
|
||||
JSON().Object().
|
||||
ContainsKey("error")
|
||||
DB.SetBackend(oldDb)
|
||||
}
|
||||
|
||||
func TestApiUpdateWithInvalidSubdomain(t *testing.T) {
|
||||
validTxtData := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
|
||||
updateJSON := map[string]interface{}{
|
||||
"subdomain": "",
|
||||
"txt": ""}
|
||||
|
||||
router := setupRouter(false, false)
|
||||
server := httptest.NewServer(router)
|
||||
defer server.Close()
|
||||
e := getExpect(t, server)
|
||||
newUser, err := DB.Register(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 := setupRouter(false, false)
|
||||
server := httptest.NewServer(router)
|
||||
defer server.Close()
|
||||
e := getExpect(t, server)
|
||||
newUser, err := DB.Register(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) {
|
||||
e := setupIris(t, false, false)
|
||||
router := setupRouter(false, false)
|
||||
server := httptest.NewServer(router)
|
||||
defer server.Close()
|
||||
e := getExpect(t, server)
|
||||
e.POST("/update").Expect().
|
||||
Status(iris.StatusUnauthorized).
|
||||
Status(http.StatusUnauthorized).
|
||||
JSON().Object().
|
||||
ContainsKey("error").
|
||||
NotContainsKey("txt")
|
||||
@@ -102,7 +270,10 @@ func TestApiUpdateWithCredentials(t *testing.T) {
|
||||
"subdomain": "",
|
||||
"txt": ""}
|
||||
|
||||
e := setupIris(t, false, false)
|
||||
router := setupRouter(false, false)
|
||||
server := httptest.NewServer(router)
|
||||
defer server.Close()
|
||||
e := getExpect(t, server)
|
||||
newUser, err := DB.Register(cidrslice{})
|
||||
if err != nil {
|
||||
t.Errorf("Could not create new user, got error [%v]", err)
|
||||
@@ -110,13 +281,12 @@ func TestApiUpdateWithCredentials(t *testing.T) {
|
||||
// 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).
|
||||
Status(http.StatusOK).
|
||||
JSON().Object().
|
||||
ContainsKey("txt").
|
||||
NotContainsKey("error").
|
||||
@@ -133,7 +303,10 @@ func TestApiUpdateWithCredentialsMockDB(t *testing.T) {
|
||||
updateJSON["subdomain"] = "a097455b-52cc-4569-90c8-7a4b97c6eba8"
|
||||
updateJSON["txt"] = validTxtData
|
||||
|
||||
e := setupIris(t, false, true)
|
||||
router := setupRouter(false, true)
|
||||
server := httptest.NewServer(router)
|
||||
defer server.Close()
|
||||
e := getExpect(t, server)
|
||||
oldDb := DB.GetBackend()
|
||||
db, mock, _ := sqlmock.New()
|
||||
DB.SetBackend(db)
|
||||
@@ -143,22 +316,19 @@ func TestApiUpdateWithCredentialsMockDB(t *testing.T) {
|
||||
e.POST("/update").
|
||||
WithJSON(updateJSON).
|
||||
Expect().
|
||||
Status(iris.StatusInternalServerError).
|
||||
Status(http.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)
|
||||
router := setupRouter(true, false)
|
||||
server := httptest.NewServer(router)
|
||||
defer server.Close()
|
||||
e := getExpect(t, server)
|
||||
// User without defined CIDR masks
|
||||
newUser, err := DB.Register(cidrslice{})
|
||||
if err != nil {
|
||||
@@ -173,7 +343,7 @@ func TestApiManyUpdateWithCredentials(t *testing.T) {
|
||||
}
|
||||
|
||||
// Another user with valid CIDR mask to match the httpexpect default
|
||||
newUserWithValidCIDR, err := DB.Register(cidrslice{"0.0.0.0/32", "invalid"})
|
||||
newUserWithValidCIDR, err := DB.Register(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)
|
||||
}
|
||||
@@ -196,13 +366,14 @@ func TestApiManyUpdateWithCredentials(t *testing.T) {
|
||||
{newUserWithValidCIDR.Username.String(), newUserWithValidCIDR.Password, newUserWithValidCIDR.Subdomain, validTxtData, 200},
|
||||
{newUser.Username.String(), "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", newUser.Subdomain, validTxtData, 401},
|
||||
} {
|
||||
updateJSON = map[string]interface{}{
|
||||
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)
|
||||
}
|
||||
@@ -210,13 +381,12 @@ func TestApiManyUpdateWithCredentials(t *testing.T) {
|
||||
|
||||
func TestApiManyUpdateWithIpCheckHeaders(t *testing.T) {
|
||||
|
||||
updateJSON := map[string]interface{}{
|
||||
"subdomain": "",
|
||||
"txt": ""}
|
||||
|
||||
e := setupIris(t, false, false)
|
||||
router := setupRouter(false, false)
|
||||
server := httptest.NewServer(router)
|
||||
defer server.Close()
|
||||
e := getExpect(t, server)
|
||||
// Use header checks from default header (X-Forwarded-For)
|
||||
DNSConf.API.UseHeader = true
|
||||
Config.API.UseHeader = true
|
||||
// User without defined CIDR masks
|
||||
newUser, err := DB.Register(cidrslice{})
|
||||
if err != nil {
|
||||
@@ -247,7 +417,7 @@ func TestApiManyUpdateWithIpCheckHeaders(t *testing.T) {
|
||||
{newUserWithIP6CIDR, "2002:c0a7:0ff::0", 401},
|
||||
{newUserWithIP6CIDR, "2002:c0a8:d3ad:b33f:c0ff:33b4:dc0d:3b4d", 200},
|
||||
} {
|
||||
updateJSON = map[string]interface{}{
|
||||
updateJSON := map[string]interface{}{
|
||||
"subdomain": test.user.Subdomain,
|
||||
"txt": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}
|
||||
e.POST("/update").
|
||||
@@ -258,5 +428,13 @@ func TestApiManyUpdateWithIpCheckHeaders(t *testing.T) {
|
||||
Expect().
|
||||
Status(test.status)
|
||||
}
|
||||
DNSConf.API.UseHeader = false
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type key int
|
||||
|
||||
// ACMETxtKey is a context key for ACMETxt struct
|
||||
const ACMETxtKey key = 0
|
||||
|
||||
// Auth middleware for update request
|
||||
func Auth(update httprouter.Handle) httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
postData := ACMETxt{}
|
||||
userOK := false
|
||||
user, err := getUserFromRequest(r)
|
||||
if err == nil {
|
||||
if updateAllowedFromIP(r, user) {
|
||||
dec := json.NewDecoder(r.Body)
|
||||
err = dec.Decode(&postData)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"error": "json_error", "string": err.Error()}).Error("Decode error")
|
||||
}
|
||||
if user.Subdomain == postData.Subdomain {
|
||||
userOK = true
|
||||
} else {
|
||||
log.WithFields(log.Fields{"error": "subdomain_mismatch", "name": postData.Subdomain, "expected": user.Subdomain}).Error("Subdomain mismatch")
|
||||
}
|
||||
} else {
|
||||
log.WithFields(log.Fields{"error": "ip_unauthorized"}).Error("Update not allowed from IP")
|
||||
}
|
||||
} else {
|
||||
log.WithFields(log.Fields{"error": err.Error()}).Error("Error while trying to get user")
|
||||
}
|
||||
if userOK {
|
||||
// Set user info to the decoded ACMETxt object
|
||||
postData.Username = user.Username
|
||||
postData.Password = user.Password
|
||||
// Set the ACMETxt struct to context to pull in from update function
|
||||
ctx := context.WithValue(r.Context(), ACMETxtKey, postData)
|
||||
update(w, r.WithContext(ctx), p)
|
||||
} else {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
_, _ = w.Write(jsonError("forbidden"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getUserFromRequest(r *http.Request) (ACMETxt, error) {
|
||||
uname := r.Header.Get("X-Api-User")
|
||||
passwd := r.Header.Get("X-Api-Key")
|
||||
username, err := getValidUsername(uname)
|
||||
if err != nil {
|
||||
return ACMETxt{}, fmt.Errorf("Invalid username: %s: %s", uname, err.Error())
|
||||
}
|
||||
if validKey(passwd) {
|
||||
dbuser, err := DB.GetByUsername(username)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"error": err.Error()}).Error("Error while trying to get user")
|
||||
// To protect against timed side channel (never gonna give you up)
|
||||
correctPassword(passwd, "$2a$10$8JEFVNYYhLoBysjAxe2yBuXrkDojBQBkVpXEQgyQyjn43SvJ4vL36")
|
||||
|
||||
return ACMETxt{}, fmt.Errorf("Invalid username: %s", uname)
|
||||
}
|
||||
if correctPassword(passwd, dbuser.Password) {
|
||||
return dbuser, nil
|
||||
}
|
||||
return ACMETxt{}, fmt.Errorf("Invalid password for user %s", uname)
|
||||
}
|
||||
return ACMETxt{}, fmt.Errorf("Invalid key for user %s", uname)
|
||||
}
|
||||
|
||||
func updateAllowedFromIP(r *http.Request, user ACMETxt) bool {
|
||||
if Config.API.UseHeader {
|
||||
ips := getIPListFromHeader(r.Header.Get(Config.API.HeaderName))
|
||||
return user.allowedFromList(ips)
|
||||
}
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"error": err.Error(), "remoteaddr": r.RemoteAddr}).Error("Error while parsing remote address")
|
||||
host = ""
|
||||
}
|
||||
return user.allowedFrom(host)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUpdateAllowedFromIP(t *testing.T) {
|
||||
Config.API.UseHeader = false
|
||||
userWithAllow := newACMETxt()
|
||||
userWithAllow.AllowFrom = cidrslice{"192.168.1.2/32", "[::1]/128"}
|
||||
userWithoutAllow := 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 := updateAllowedFromIP(newreq, userWithAllow)
|
||||
if test.expected != ret {
|
||||
t.Errorf("Test %d: Unexpected result for user with allowForm set", i)
|
||||
}
|
||||
|
||||
if !updateAllowedFromIP(newreq, userWithoutAllow) {
|
||||
t.Errorf("Test %d: Unexpected result for user without allowForm set", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package main
|
||||
|
||||
import "github.com/go-acme/lego/challenge/dns01"
|
||||
|
||||
// ChallengeProvider implements go-acme/lego Provider interface which is used for ACME DNS challenge handling
|
||||
type ChallengeProvider struct {
|
||||
servers []*DNSServer
|
||||
}
|
||||
|
||||
// NewChallengeProvider creates a new instance of ChallengeProvider
|
||||
func NewChallengeProvider(servers []*DNSServer) ChallengeProvider {
|
||||
return ChallengeProvider{servers: servers}
|
||||
}
|
||||
|
||||
// Present is used for making the ACME DNS challenge token available for DNS
|
||||
func (c *ChallengeProvider) Present(_, _, keyAuth string) error {
|
||||
_, token := dns01.GetRecord("whatever", keyAuth)
|
||||
for _, s := range c.servers {
|
||||
s.PersonalKeyAuth = token
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanUp is called after the run to remove the ACME DNS challenge tokens from DNS records
|
||||
func (c *ChallengeProvider) CleanUp(_, _, _ string) error {
|
||||
for _, s := range c.servers {
|
||||
s.PersonalKeyAuth = ""
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+27
-22
@@ -1,24 +1,22 @@
|
||||
[general]
|
||||
# dns interface
|
||||
listen = ":53"
|
||||
# protocol, "udp", "udp4", "udp6" or "tcp", "tcp4", "tcp6"
|
||||
protocol = "udp"
|
||||
# domain name to serve the requests off of
|
||||
# DNS interface. Note that systemd-resolved may reserve port 53 on 127.0.0.53
|
||||
# In this case acme-dns will error out and you will need to define the listening interface
|
||||
# for example: listen = "127.0.0.1:53"
|
||||
listen = "127.0.0.1:53"
|
||||
# protocol, "both", "both4", "both6", "udp", "udp4", "udp6" or "tcp", "tcp4", "tcp6"
|
||||
protocol = "both"
|
||||
# domain name to serve the requests off of
|
||||
domain = "auth.example.org"
|
||||
# zone name server
|
||||
nsname = "ns1.auth.example.org"
|
||||
# zone name server
|
||||
nsname = "auth.example.org"
|
||||
# admin email address, where @ is substituted with .
|
||||
nsadmin = "admin.example.org"
|
||||
# predefined records served in addition to the TXT
|
||||
records = [
|
||||
# default A
|
||||
"auth.example.org. A 192.168.1.100",
|
||||
# A
|
||||
"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.",
|
||||
# domain pointing to the public IP of your acme-dns server
|
||||
"auth.example.org. A 198.51.100.1",
|
||||
# specify that auth.example.org will resolve any *.auth.example.org records
|
||||
"auth.example.org. NS auth.example.org.",
|
||||
]
|
||||
# debug messages from CORS etc
|
||||
debug = false
|
||||
@@ -27,19 +25,26 @@ debug = false
|
||||
# Database engine to use, sqlite3 or postgres
|
||||
engine = "sqlite3"
|
||||
# Connection string, filename for sqlite3 and postgres://$username:$password@$host/$db_name for postgres
|
||||
connection = "acme-dns.db"
|
||||
# Please note that the default Docker image uses path /var/lib/acme-dns/acme-dns.db for sqlite3
|
||||
connection = "/var/lib/acme-dns/acme-dns.db"
|
||||
# connection = "postgres://user:password@localhost/acmedns_db"
|
||||
|
||||
[api]
|
||||
# domain name to listen requests for, mandatory if using tls = "letsencrypt"
|
||||
api_domain = ""
|
||||
# listen ip eg. 127.0.0.1
|
||||
ip = "0.0.0.0"
|
||||
# disable registration endpoint
|
||||
disable_registration = false
|
||||
# listen port, eg. 443 for default HTTPS
|
||||
port = "8080"
|
||||
# possible values: "letsencrypt", "cert", "none"
|
||||
tls = "none"
|
||||
port = "443"
|
||||
# possible values: "letsencrypt", "letsencryptstaging", "cert", "none"
|
||||
tls = "letsencryptstaging"
|
||||
# only used if tls = "cert"
|
||||
tls_cert_privkey = "/etc/tls/example.org/privkey.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
|
||||
corsorigins = [
|
||||
"*"
|
||||
@@ -56,5 +61,5 @@ loglevel = "debug"
|
||||
logtype = "stdout"
|
||||
# file path for logfile TODO
|
||||
# logfile = "./acme-dns.log"
|
||||
# format, either "json" or "text"
|
||||
# format, either "json" or "text"
|
||||
logformat = "text"
|
||||
|
||||
@@ -4,29 +4,53 @@ import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/google/uuid"
|
||||
_ "github.com/lib/pq"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/satori/go.uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var recordsTable = `
|
||||
// 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,
|
||||
Value TEXT,
|
||||
LastActive INT,
|
||||
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]")
|
||||
re, _ := regexp.Compile(`\$[0-9]`)
|
||||
return re.ReplaceAllString(s, "?")
|
||||
}
|
||||
|
||||
@@ -38,44 +62,151 @@ func (d *acmedb) Init(engine string, connection string) error {
|
||||
return err
|
||||
}
|
||||
d.DB = db
|
||||
//d.DB.SetMaxOpenConns(1)
|
||||
_, err = d.DB.Exec(recordsTable)
|
||||
// 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 == "sqlite3" {
|
||||
_, _ = 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 err
|
||||
}
|
||||
|
||||
func (d *acmedb) 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 *acmedb) handleDBUpgrades(version int) error {
|
||||
if version == 0 {
|
||||
return d.handleDBUpgradeTo1()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *acmedb) handleDBUpgradeTo1() error {
|
||||
var err error
|
||||
var subdomains []string
|
||||
rows, err := d.DB.Query("SELECT Subdomain FROM records")
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"error": err.Error()}).Error("Error in DB upgrade")
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var subdomain string
|
||||
err = rows.Scan(&subdomain)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"error": err.Error()}).Error("Error in DB upgrade while reading values")
|
||||
return err
|
||||
}
|
||||
subdomains = append(subdomains, subdomain)
|
||||
}
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"error": err.Error()}).Error("Error in DB upgrade while inserting values")
|
||||
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 {
|
||||
log.WithFields(log.Fields{"error": err.Error()}).Error("Error in DB upgrade while inserting values")
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
// SQLite doesn't support dropping columns
|
||||
if Config.Database.Engine != "sqlite3" {
|
||||
_, _ = 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
|
||||
}
|
||||
|
||||
// Create two rows for subdomain to the txt table
|
||||
func (d *acmedb) 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 *acmedb) Register(afrom cidrslice) (ACMETxt, error) {
|
||||
d.Lock()
|
||||
defer d.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 := 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" {
|
||||
values($1, $2, $3, $4)`
|
||||
if Config.Database.Engine == "sqlite3" {
|
||||
regSQL = getSQLiteStmt(regSQL)
|
||||
}
|
||||
sm, err := d.DB.Prepare(regSQL)
|
||||
sm, err := tx.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
|
||||
_, err = sm.Exec(a.Username.String(), passwordHash, a.Subdomain, a.AllowFrom.JSON())
|
||||
if err == nil {
|
||||
err = d.NewTXTValuesInTransaction(tx, a.Subdomain)
|
||||
}
|
||||
return a, nil
|
||||
return a, err
|
||||
}
|
||||
|
||||
func (d *acmedb) GetByUsername(u uuid.UUID) (ACMETxt, error) {
|
||||
@@ -83,11 +214,11 @@ func (d *acmedb) GetByUsername(u uuid.UUID) (ACMETxt, error) {
|
||||
defer d.Unlock()
|
||||
var results []ACMETxt
|
||||
getSQL := `
|
||||
SELECT Username, Password, Subdomain, Value, LastActive, AllowFrom
|
||||
SELECT Username, Password, Subdomain, AllowFrom
|
||||
FROM records
|
||||
WHERE Username=$1 LIMIT 1
|
||||
`
|
||||
if DNSConf.Database.Engine == "sqlite3" {
|
||||
if Config.Database.Engine == "sqlite3" {
|
||||
getSQL = getSQLiteStmt(getSQL)
|
||||
}
|
||||
|
||||
@@ -116,51 +247,53 @@ func (d *acmedb) GetByUsername(u uuid.UUID) (ACMETxt, error) {
|
||||
return ACMETxt{}, errors.New("no user")
|
||||
}
|
||||
|
||||
func (d *acmedb) GetByDomain(domain string) ([]ACMETxt, error) {
|
||||
func (d *acmedb) GetTXTForDomain(domain string) ([]string, error) {
|
||||
d.Lock()
|
||||
defer d.Unlock()
|
||||
domain = sanitizeString(domain)
|
||||
var a []ACMETxt
|
||||
var txts []string
|
||||
getSQL := `
|
||||
SELECT Username, Password, Subdomain, Value, LastActive, AllowFrom
|
||||
FROM records
|
||||
WHERE Subdomain=$1 LIMIT 1
|
||||
SELECT Value FROM txt WHERE Subdomain=$1 LIMIT 2
|
||||
`
|
||||
if DNSConf.Database.Engine == "sqlite3" {
|
||||
if Config.Database.Engine == "sqlite3" {
|
||||
getSQL = getSQLiteStmt(getSQL)
|
||||
}
|
||||
|
||||
sm, err := d.DB.Prepare(getSQL)
|
||||
if err != nil {
|
||||
return a, err
|
||||
return txts, err
|
||||
}
|
||||
defer sm.Close()
|
||||
rows, err := sm.Query(domain)
|
||||
if err != nil {
|
||||
return a, err
|
||||
return txts, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
txt, err := getModelFromRow(rows)
|
||||
var rtxt string
|
||||
err = rows.Scan(&rtxt)
|
||||
if err != nil {
|
||||
return a, err
|
||||
return txts, err
|
||||
}
|
||||
a = append(a, txt)
|
||||
txts = append(txts, rtxt)
|
||||
}
|
||||
return a, nil
|
||||
return txts, nil
|
||||
}
|
||||
|
||||
func (d *acmedb) Update(a ACMETxt) error {
|
||||
func (d *acmedb) Update(a ACMETxtPost) error {
|
||||
d.Lock()
|
||||
defer d.Unlock()
|
||||
var err error
|
||||
// Data in a is already sanitized
|
||||
timenow := time.Now().Unix()
|
||||
|
||||
updSQL := `
|
||||
UPDATE records SET Value=$1, LastActive=$2
|
||||
WHERE Username=$3 AND Subdomain=$4
|
||||
UPDATE txt SET Value=$1, LastUpdate=$2
|
||||
WHERE rowid=(
|
||||
SELECT rowid FROM txt WHERE Subdomain=$3 ORDER BY LastUpdate LIMIT 1)
|
||||
`
|
||||
if DNSConf.Database.Engine == "sqlite3" {
|
||||
if Config.Database.Engine == "sqlite3" {
|
||||
updSQL = getSQLiteStmt(updSQL)
|
||||
}
|
||||
|
||||
@@ -169,7 +302,7 @@ func (d *acmedb) Update(a ACMETxt) error {
|
||||
return err
|
||||
}
|
||||
defer sm.Close()
|
||||
_, err = sm.Exec(a.Value, timenow, a.Username, a.Subdomain)
|
||||
_, err = sm.Exec(a.Value, timenow, a.Subdomain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -183,8 +316,6 @@ func getModelFromRow(r *sql.Rows) (ACMETxt, error) {
|
||||
&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")
|
||||
|
||||
+33
-37
@@ -67,10 +67,10 @@ func TestRegisterMany(t *testing.T) {
|
||||
t.Errorf("Test %d: Got error when fetching username: [%v]", i, err)
|
||||
}
|
||||
if len(user.AllowFrom) != len(test.output) {
|
||||
t.Errorf("Test %d: Expected to recieve struct with [%d] entries in AllowFrom, but got [%d] records", i, len(test.output), len(user.AllowFrom))
|
||||
t.Errorf("Test %d: Expected to receive struct with [%d] entries in AllowFrom, but got [%d] records", i, len(test.output), len(user.AllowFrom))
|
||||
}
|
||||
if len(res.AllowFrom) != len(test.output) {
|
||||
t.Errorf("Test %d: Expected to recieve struct with [%d] entries in AllowFrom, but got [%d] records", i, len(test.output), len(res.AllowFrom))
|
||||
t.Errorf("Test %d: Expected to receive struct with [%d] entries in AllowFrom, but got [%d] records", i, len(test.output), len(res.AllowFrom))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -118,7 +118,7 @@ func TestPrepareErrors(t *testing.T) {
|
||||
t.Errorf("Expected error, but didn't get one")
|
||||
}
|
||||
|
||||
_, err = DB.GetByDomain(reg.Subdomain)
|
||||
_, err = DB.GetTXTForDomain(reg.Subdomain)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, but didn't get one")
|
||||
}
|
||||
@@ -151,7 +151,7 @@ func TestQueryExecErrors(t *testing.T) {
|
||||
t.Errorf("Expected error from exec, but got none")
|
||||
}
|
||||
|
||||
_, err = DB.GetByDomain(reg.Subdomain)
|
||||
_, err = DB.GetTXTForDomain(reg.Subdomain)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error from exec in GetByDomain, but got none")
|
||||
}
|
||||
@@ -161,7 +161,7 @@ func TestQueryExecErrors(t *testing.T) {
|
||||
t.Errorf("Expected error from exec in Register, but got none")
|
||||
}
|
||||
reg.Value = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
err = DB.Update(reg)
|
||||
err = DB.Update(reg.ACMETxtPost)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error from exec in Update, but got none")
|
||||
}
|
||||
@@ -195,11 +195,6 @@ func TestQueryScanErrors(t *testing.T) {
|
||||
if err == nil {
|
||||
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) {
|
||||
@@ -226,46 +221,55 @@ func TestBadDBValues(t *testing.T) {
|
||||
t.Errorf("Expected error from scan in, but got none")
|
||||
}
|
||||
|
||||
_, err = DB.GetByDomain(reg.Subdomain)
|
||||
_, err = DB.GetTXTForDomain(reg.Subdomain)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error from scan in GetByDomain, but got none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetByDomain(t *testing.T) {
|
||||
var regDomain = ACMETxt{}
|
||||
|
||||
func TestGetTXTForDomain(t *testing.T) {
|
||||
// Create reg to refer to
|
||||
reg, err := DB.Register(cidrslice{})
|
||||
if err != nil {
|
||||
t.Errorf("Registration failed, got error [%v]", err)
|
||||
}
|
||||
|
||||
regDomainSlice, err := DB.GetByDomain(reg.Subdomain)
|
||||
txtval1 := "___validation_token_received_from_the_ca___"
|
||||
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 {
|
||||
t.Errorf("Could not get test user, got error [%v]", err)
|
||||
}
|
||||
if len(regDomainSlice) == 0 {
|
||||
t.Errorf("No rows returned for GetByDomain [%s]", reg.Subdomain)
|
||||
} else {
|
||||
regDomain = regDomainSlice[0]
|
||||
t.Errorf("No rows returned for GetTXTForDomain [%s]", reg.Subdomain)
|
||||
}
|
||||
|
||||
if reg.Username != regDomain.Username {
|
||||
t.Errorf("GetByUsername username [%q] did not match the original [%q]", regDomain.Username, reg.Username)
|
||||
var val1found = false
|
||||
var val2found = false
|
||||
for _, v := range regDomainSlice {
|
||||
if v == txtval1 {
|
||||
val1found = true
|
||||
}
|
||||
if v == txtval2 {
|
||||
val2found = true
|
||||
}
|
||||
}
|
||||
|
||||
if reg.Subdomain != regDomain.Subdomain {
|
||||
t.Errorf("GetByUsername subdomain [%q] did not match the original [%q]", regDomain.Subdomain, reg.Subdomain)
|
||||
if !val1found {
|
||||
t.Errorf("No TXT value found for val1")
|
||||
}
|
||||
|
||||
// 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)
|
||||
if !val2found {
|
||||
t.Errorf("No TXT value found for val2")
|
||||
}
|
||||
|
||||
// Not found
|
||||
regNotfound, _ := DB.GetByDomain("does-not-exist")
|
||||
regNotfound, _ := DB.GetTXTForDomain("does-not-exist")
|
||||
if len(regNotfound) > 0 {
|
||||
t.Errorf("No records should be returned.")
|
||||
}
|
||||
@@ -290,16 +294,8 @@ func TestUpdate(t *testing.T) {
|
||||
regUser.Password = "nevergonnagiveyouup"
|
||||
regUser.Value = validTXT
|
||||
|
||||
err = DB.Update(regUser)
|
||||
err = DB.Update(regUser.ACMETxtPost)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,105 +2,244 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/miekg/dns"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func readQuery(m *dns.Msg) {
|
||||
for _, que := range m.Question {
|
||||
if rr, rc, err := answer(que); err == nil {
|
||||
m.MsgHdr.Rcode = rc
|
||||
for _, r := range rr {
|
||||
m.Answer = append(m.Answer, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Records is a slice of ResourceRecords
|
||||
type Records struct {
|
||||
Records []dns.RR
|
||||
}
|
||||
|
||||
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)
|
||||
// DNSServer is the main struct for acme-dns DNS server
|
||||
type DNSServer struct {
|
||||
DB database
|
||||
Domain string
|
||||
Server *dns.Server
|
||||
SOA dns.RR
|
||||
PersonalKeyAuth string
|
||||
Domains map[string]Records
|
||||
}
|
||||
|
||||
// NewDNSServer parses the DNS records from config and returns a new DNSServer struct
|
||||
func NewDNSServer(db database, addr string, proto string, domain string) *DNSServer {
|
||||
var server DNSServer
|
||||
server.Server = &dns.Server{Addr: addr, Net: proto}
|
||||
if !strings.HasSuffix(domain, ".") {
|
||||
domain = domain + "."
|
||||
}
|
||||
server.Domain = strings.ToLower(domain)
|
||||
server.DB = db
|
||||
server.PersonalKeyAuth = ""
|
||||
server.Domains = make(map[string]Records)
|
||||
return &server
|
||||
}
|
||||
|
||||
// Start starts the DNSServer
|
||||
func (d *DNSServer) Start(errorChannel chan error) {
|
||||
// DNS server part
|
||||
dns.HandleFunc(".", d.handleRequest)
|
||||
log.WithFields(log.Fields{"addr": d.Server.Addr, "proto": d.Server.Net}).Info("Listening DNS")
|
||||
err := d.Server.ListenAndServe()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"error": err.Error()}).Debug("Error while trying to get record")
|
||||
return ra, dns.RcodeNameError, err
|
||||
errorChannel <- 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 {
|
||||
// ParseRecords parses a slice of DNS record string
|
||||
func (d *DNSServer) ParseRecords(config DNSConfig) {
|
||||
for _, v := range config.General.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)
|
||||
// Add parsed RR
|
||||
d.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(config.Domain), strings.ToLower(config.Nsname), strings.ToLower(config.Nsadmin), serial)
|
||||
SOAstring := fmt.Sprintf("%s. SOA %s. %s. %s 28800 7200 604800 86400", strings.ToLower(config.General.Domain), strings.ToLower(config.General.Nsname), strings.ToLower(config.General.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)
|
||||
d.appendRR(soarr)
|
||||
d.SOA = 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]
|
||||
func (d *DNSServer) appendRR(rr dns.RR) {
|
||||
addDomain := rr.Header().Name
|
||||
_, ok := d.Domains[addDomain]
|
||||
if !ok {
|
||||
newrr := make(map[string][]dns.RR)
|
||||
rrmap[rr.Header().Rrtype] = newrr
|
||||
d.Domains[addDomain] = Records{[]dns.RR{rr}}
|
||||
} else {
|
||||
drecs := d.Domains[addDomain]
|
||||
drecs.Records = append(drecs.Records, rr)
|
||||
d.Domains[addDomain] = drecs
|
||||
}
|
||||
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
|
||||
log.WithFields(log.Fields{"recordtype": dns.TypeToString[rr.Header().Rrtype], "domain": addDomain}).Debug("Adding new record to domain")
|
||||
}
|
||||
|
||||
func (d *DNSServer) 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.MsgHdr.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 {
|
||||
d.readQuery(m)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if r.Opcode == dns.OpcodeQuery {
|
||||
d.readQuery(m)
|
||||
}
|
||||
}
|
||||
_ = w.WriteMsg(m)
|
||||
}
|
||||
|
||||
func (d *DNSServer) readQuery(m *dns.Msg) {
|
||||
var authoritative = false
|
||||
for _, que := range m.Question {
|
||||
if rr, rc, auth, err := d.answer(que); err == nil {
|
||||
if auth {
|
||||
authoritative = auth
|
||||
}
|
||||
m.MsgHdr.Rcode = rc
|
||||
m.Answer = append(m.Answer, rr...)
|
||||
}
|
||||
}
|
||||
m.MsgHdr.Authoritative = authoritative
|
||||
if authoritative {
|
||||
if m.MsgHdr.Rcode == dns.RcodeNameError {
|
||||
m.Ns = append(m.Ns, d.SOA)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DNSServer) getRecord(q dns.Question) ([]dns.RR, error) {
|
||||
var rr []dns.RR
|
||||
var cnames []dns.RR
|
||||
domain, ok := d.Domains[strings.ToLower(q.Name)]
|
||||
if !ok {
|
||||
return rr, fmt.Errorf("No records for domain %s", q.Name)
|
||||
}
|
||||
for _, ri := range domain.Records {
|
||||
if ri.Header().Rrtype == q.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
|
||||
}
|
||||
|
||||
// answeringForDomain checks if we have any records for a domain
|
||||
func (d *DNSServer) answeringForDomain(name string) bool {
|
||||
if d.Domain == strings.ToLower(name) {
|
||||
return true
|
||||
}
|
||||
_, ok := d.Domains[strings.ToLower(name)]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (d *DNSServer) isAuthoritative(q dns.Question) bool {
|
||||
if d.answeringForDomain(q.Name) {
|
||||
return true
|
||||
}
|
||||
domainParts := strings.Split(strings.ToLower(q.Name), ".")
|
||||
for i := range domainParts {
|
||||
if d.answeringForDomain(strings.Join(domainParts[i:], ".")) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isOwnChallenge checks if the query is for the domain of this acme-dns instance. Used for answering its own ACME challenges
|
||||
func (d *DNSServer) isOwnChallenge(name string) bool {
|
||||
domainParts := strings.SplitN(name, ".", 2)
|
||||
if len(domainParts) == 2 {
|
||||
if strings.ToLower(domainParts[0]) == "_acme-challenge" {
|
||||
domain := strings.ToLower(domainParts[1])
|
||||
if !strings.HasSuffix(domain, ".") {
|
||||
domain = domain + "."
|
||||
}
|
||||
if domain == d.Domain {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *DNSServer) answer(q dns.Question) ([]dns.RR, int, bool, error) {
|
||||
var rcode int
|
||||
var err error
|
||||
var txtRRs []dns.RR
|
||||
var authoritative = d.isAuthoritative(q)
|
||||
if !d.isOwnChallenge(q.Name) && !d.answeringForDomain(q.Name) {
|
||||
rcode = dns.RcodeNameError
|
||||
}
|
||||
r, _ := d.getRecord(q)
|
||||
if q.Qtype == dns.TypeTXT {
|
||||
if d.isOwnChallenge(q.Name) {
|
||||
txtRRs, err = d.answerOwnChallenge(q)
|
||||
} else {
|
||||
txtRRs, err = d.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
|
||||
}
|
||||
log.WithFields(log.Fields{"qtype": dns.TypeToString[q.Qtype], "domain": q.Name, "rcode": dns.RcodeToString[rcode]}).Debug("Answering question for domain")
|
||||
return r, rcode, authoritative, nil
|
||||
}
|
||||
|
||||
func (d *DNSServer) answerTXT(q dns.Question) ([]dns.RR, error) {
|
||||
var ra []dns.RR
|
||||
subdomain := sanitizeDomainQuestion(q.Name)
|
||||
atxt, err := d.DB.GetTXTForDomain(subdomain)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"error": err.Error()}).Debug("Error while trying to get record")
|
||||
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
|
||||
}
|
||||
|
||||
// answerOwnChallenge answers to ACME challenge for acme-dns own certificate
|
||||
func (d *DNSServer) answerOwnChallenge(q dns.Question) ([]dns.RR, error) {
|
||||
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, d.PersonalKeyAuth)
|
||||
return []dns.RR{r}, nil
|
||||
}
|
||||
|
||||
+146
-60
@@ -5,33 +5,30 @@ import (
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"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) {
|
||||
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 []dns.RR{}, fmt.Errorf("Error querying the server [%v]", err)
|
||||
return in, 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, fmt.Errorf("Received error from the server [%s]", dns.RcodeToString[in.Rcode])
|
||||
}
|
||||
|
||||
return in.Answer, nil
|
||||
return in, nil
|
||||
}
|
||||
|
||||
func hasExpectedTXTAnswer(answer []dns.RR, cmpTXT string) error {
|
||||
@@ -51,25 +48,6 @@ func hasExpectedTXTAnswer(answer []dns.RR, cmpTXT string) error {
|
||||
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"}
|
||||
@@ -88,44 +66,36 @@ func TestQuestionDBError(t *testing.T) {
|
||||
defer DB.SetBackend(oldDb)
|
||||
|
||||
q := dns.Question{Name: dns.Fqdn("whatever.tld"), Qtype: dns.TypeTXT, Qclass: dns.ClassINET}
|
||||
_, rcode, err := answerTXT(q)
|
||||
_, err = dnsserver.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 testcfg = DNSConfig{
|
||||
General: general{
|
||||
Domain: ")",
|
||||
Nsname: "ns1.auth.example.org",
|
||||
Nsadmin: "admin.example.org",
|
||||
StaticRecords: []string{},
|
||||
Debug: false,
|
||||
},
|
||||
}
|
||||
var testRR Records
|
||||
testRR.Parse(testcfg)
|
||||
dnsserver.ParseRecords(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"}
|
||||
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) > 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 {
|
||||
if len(answer.Answer) == 0 {
|
||||
t.Error("No answer for DNS query")
|
||||
}
|
||||
|
||||
@@ -135,8 +105,98 @@ func TestResolveA(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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.MsgHdr.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.MsgHdr.Authoritative {
|
||||
t.Errorf("Authoritative bit should not be set for non-authoritative domain.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveTXT(t *testing.T) {
|
||||
resolv := resolver{server: "0.0.0.0:15353"}
|
||||
resolv := resolver{server: "127.0.0.1:15353"}
|
||||
validTXT := "______________valid_response_______________"
|
||||
|
||||
atxt, err := DB.Register(cidrslice{})
|
||||
@@ -145,7 +205,7 @@ func TestResolveTXT(t *testing.T) {
|
||||
return
|
||||
}
|
||||
atxt.Value = validTXT
|
||||
err = DB.Update(atxt)
|
||||
err = DB.Update(atxt.ACMETxtPost)
|
||||
if err != nil {
|
||||
t.Errorf("Could not update db record: [%v]", err)
|
||||
return
|
||||
@@ -172,18 +232,20 @@ func TestResolveTXT(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
if len(answer) > 0 {
|
||||
if !test.getAnswer {
|
||||
if len(answer.Answer) > 0 {
|
||||
if !test.getAnswer && answer.Answer[0].Header().Rrtype != dns.TypeSOA {
|
||||
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)
|
||||
if test.getAnswer {
|
||||
err = hasExpectedTXTAnswer(answer.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 {
|
||||
@@ -193,3 +255,27 @@ func TestResolveTXT(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
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
|
||||
@@ -0,0 +1,38 @@
|
||||
module github.com/joohoi/acme-dns
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v0.3.1
|
||||
github.com/DATA-DOG/go-sqlmock v1.3.3
|
||||
github.com/ajg/form v1.5.1 // indirect
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5
|
||||
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072 // indirect
|
||||
github.com/gavv/httpexpect v2.0.0+incompatible
|
||||
github.com/go-acme/lego v2.7.2+incompatible
|
||||
github.com/go-acme/lego/v3 v3.1.0
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/gorilla/websocket v1.4.1 // indirect
|
||||
github.com/imkira/go-interpol v1.1.0 // indirect
|
||||
github.com/julienschmidt/httprouter v1.3.0
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect
|
||||
github.com/lib/pq v1.2.0
|
||||
github.com/mattn/go-colorable v0.1.4 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.11.0
|
||||
github.com/mholt/certmagic v0.8.1-0.20191019173955-6f9f0e6dd0e8
|
||||
github.com/miekg/dns v1.1.22
|
||||
github.com/moul/http2curl v1.0.0 // indirect
|
||||
github.com/rs/cors v1.7.0
|
||||
github.com/sergi/go-diff v1.0.0 // indirect
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/valyala/fasthttp v1.5.0 // 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
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
|
||||
golang.org/x/net v0.0.0-20191014212845-da9a3fd4c582 // indirect
|
||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 // indirect
|
||||
)
|
||||
@@ -0,0 +1,393 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
contrib.go.opencensus.io/exporter/ocagent v0.4.12/go.mod h1:450APlNTSR6FrvC3CTRqYosuDstRB9un7SOx2k/9ckA=
|
||||
github.com/Azure/azure-sdk-for-go v32.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||
github.com/Azure/go-autorest/autorest v0.1.0/go.mod h1:AKyIcETwSUFxIcs/Wnq/C+kwCtlEYGUVd7FPNb2slmg=
|
||||
github.com/Azure/go-autorest/autorest v0.5.0/go.mod h1:9HLKlQjVBH6U3oDfsXOeVc56THsLPw1L03yban4xThw=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.1.0/go.mod h1:MeS4XhScH55IST095THyTxElntu7WqB7pNbZo8Q5G3E=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.2.0/go.mod h1:MeS4XhScH55IST095THyTxElntu7WqB7pNbZo8Q5G3E=
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.1.0/go.mod h1:Gf7/i2FUpyb/sGBLIFxTBzrNzBo7aPXXE3ZVeDRwdpM=
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.1.0/go.mod h1:Dk8CUAt/b/PzkfeRsWzVG9Yj3ps8mS8ECztu43rdU8U=
|
||||
github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
|
||||
github.com/Azure/go-autorest/autorest/to v0.2.0/go.mod h1:GunWKJp1AEqgMaGLV+iocmRAJWqST1wQYhyyjXJ3SJc=
|
||||
github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQvokg3NZAlQTalVMtOIAs1aGK7G6u8=
|
||||
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
|
||||
github.com/Azure/go-autorest/tracing v0.1.0/go.mod h1:ROEEAFwXycQw7Sn3DXNtEedEvdeRAgDr0izn4z5Ij88=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08=
|
||||
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87/go.mod h1:iGLljf5n9GjT6kc0HBvyI1nOKnGQbNB66VzSNbK5iks=
|
||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang v0.9.0/go.mod h1:zpDJeKyp9ScW4NNrbdr+Eyxvry3ilGPewKoXw3XGN1k=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190808125512-07798873deee/go.mod h1:myCDvQSzCW+wB1WAlocEru4wMGJxy+vlxHdhegi1CDQ=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190307165228-86c17b95fcd5/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
|
||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/aws/aws-sdk-go v1.23.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c=
|
||||
github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/cloudflare-go v0.10.2/go.mod h1:qhVI5MKwBGhdNU89ZRz2plgYutcJ5PCekLxXn56w6SY=
|
||||
github.com/cpu/goacmedns v0.0.1/go.mod h1:sesf/pNnCYwUevQEQfEwY0Y3DydlQWSGZbaMElOWxok=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
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/decker502/dnspod-go v0.2.0/go.mod h1:qsurYu1FgxcDwfSwXJdLt4kRsBLZeosEb9uq4Sy+08g=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
|
||||
github.com/dnaeon/go-vcr v0.0.0-20180814043457-aafff18a5cc2/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
|
||||
github.com/dnsimple/dnsimple-go v0.30.0/go.mod h1:O5TJ0/U6r7AfT8niYNlmohpLbCSG+c71tQlGr9SeGrg=
|
||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
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/exoscale/egoscale v0.18.1/go.mod h1:Z7OOdzzTOz1Q1PjQXumlz9Wn/CddH0zSYdCF3rnBKXE=
|
||||
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 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
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/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-acme/lego v2.7.2+incompatible h1:ThhpPBgf6oa9X/vRd0kEmWOsX7+vmYdckmGZSb+FEp0=
|
||||
github.com/go-acme/lego v2.7.2+incompatible/go.mod h1:yzMNe9CasVUhkquNvti5nAtPmG94USbYxYrZfTkIn0M=
|
||||
github.com/go-acme/lego/v3 v3.1.0 h1:yanYFoYW8azFkCvJfIk7edWWfjkYkhDxe45ZsxoW4Xk=
|
||||
github.com/go-acme/lego/v3 v3.1.0/go.mod h1:074uqt+JS6plx+c9Xaiz6+L+GBb+7itGtzfcDM2AhEE=
|
||||
github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-ini/ini v1.44.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gophercloud/gophercloud v0.3.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
|
||||
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/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhKWFeDesPjMj+wCHReeknARU3wqlyN4=
|
||||
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/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
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.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM=
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.8.2 h1:Bx0qjetmNjdFXASH02NSAREKpiaDwkO1DRZ3dV2KCcs=
|
||||
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w=
|
||||
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/kolo/xmlrpc v0.0.0-20190717152603-07c4ee3fd181/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/labbsr0x/bindman-dns-webhook v1.0.2/go.mod h1:p6b+VCXIR8NYKpDr8/dg1HKfQoRHCdcsROXKvmoehKA=
|
||||
github.com/labbsr0x/goh v1.0.1/go.mod h1:8K2UhVoaWXcCU7Lxoa2omWnC8gyW8px7/lmO61c027w=
|
||||
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/linode/linodego v0.10.0/go.mod h1:cziNP7pbvE3mXIPneHj0oRY8L1WtGEIKlZ8LANE4eXA=
|
||||
github.com/liquidweb/liquidweb-go v1.6.0/go.mod h1:UDcVnAMDkZxpw4Y7NOHkqoeiGacVLEIG/i5J9cyixzQ=
|
||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q=
|
||||
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-tty v0.0.0-20180219170247-931426f7535a/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mholt/certmagic v0.8.1-0.20191019173955-6f9f0e6dd0e8 h1:pO1zxTxNYsM0TBfLiPNzIO39D1xNwHepOiwY2WI8lWs=
|
||||
github.com/mholt/certmagic v0.8.1-0.20191019173955-6f9f0e6dd0e8/go.mod h1:91uJzK5K8IWtYQqTi5R2tsxV1pCde+wdGfaRaOZi6aQ=
|
||||
github.com/miekg/dns v1.1.15/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.22 h1:Jm64b3bO9kP43ddLjL2EY3Io6bmy1qGb9Xxz6TqS6rc=
|
||||
github.com/miekg/dns v1.1.22/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-vnc v0.0.0-20150629162542-723ed9867aed/go.mod h1:3rdaFaCv4AyBgu5ALFM0+tSuHrBh6v692nyQe3ikrq0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||
github.com/nrdcg/auroradns v1.0.0/go.mod h1:6JPXKzIRzZzMqtTDgueIhTi6rFf1QvYE/HzqidhOhjw=
|
||||
github.com/nrdcg/goinwx v0.6.1/go.mod h1:XPiut7enlbEdntAqalBIqcYcTEVhpv/dKWgDCX2SwKQ=
|
||||
github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw=
|
||||
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
||||
github.com/oracle/oci-go-sdk v7.0.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888=
|
||||
github.com/ovh/go-ovh v0.0.0-20181109152953-ba5adb4cf014/go.mod h1:joRatxRJaZBsY3JAOEMcoOp05CnZzsx4scTxi95DHyQ=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
|
||||
github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2/go.mod h1:7tZKcyumwBO6qip7RNQ5r77yrssm9bfCowcLEBcU5IA=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
|
||||
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sacloud/libsacloud v1.26.1/go.mod h1:79ZwATmHLIFZIMd7sxA3LwzVy/B77uj3LDoToVTxDoQ=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/timewasted/linode v0.0.0-20160829202747-37e84520dcf7/go.mod h1:imsgLplxEC/etjIhdr3dNzV3JeT27LbVu5pYWm0JCBY=
|
||||
github.com/transip/gotransip v0.0.0-20190812104329-6d8d9179b66f/go.mod h1:i0f4R4o2HM0m3DZYQWsj6/MEowD57VzoH0v3d7igeFY=
|
||||
github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g=
|
||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
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.5.0 h1:dhq+O9pmNZFF6qAXpasMO1xSm7dL4qEz2ylfZN8BG9w=
|
||||
github.com/valyala/fasthttp v1.5.0/go.mod h1:eriCz9OhZjKCGfJ185a/IDgNl0bg9IbzfpcslMZXU1c=
|
||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
||||
github.com/vultr/govultr v0.1.4/go.mod h1:9H008Uxr/C4vFNGLqKx232C206GL0PBHzOP0809bGNA=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/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.1.0/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
|
||||
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=
|
||||
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/ratelimit v0.0.0-20180316092928-c15da0234277/go.mod h1:2X8KaoNd1J0lZV+PxJk/5+DGbO/tpwLR1m++a7FnB/Y=
|
||||
golang.org/x/crypto v0.0.0-20180621125126-a49355c7e3f8/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20180611182652-db08ff08e862/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/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-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191014212845-da9a3fd4c582 h1:p9xBe/w/OzkeYVKm234g55gMdD1nSIooTir5kV11kfA=
|
||||
golang.org/x/net v0.0.0-20191014212845-da9a3fd4c582/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180622082034-63fc586f45fe/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
|
||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE=
|
||||
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.44.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ns1/ns1-go.v2 v2.0.0-20190730140822-b51389932cbc/go.mod h1:VV+3haRsgDiVLxyifmMBrBIuCWFBPYKbRssXB9z67Hw=
|
||||
gopkg.in/resty.v1 v1.9.1/go.mod h1:vo52Hzryw9PnPHcJfPsBiFW62XhNx5OczbV9y+IMpgc=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4=
|
||||
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
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.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
@@ -3,66 +3,211 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/iris-contrib/middleware/cors"
|
||||
"github.com/kataras/iris"
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
stdlog "log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/go-acme/lego/v3/challenge/dns01"
|
||||
legolog "github.com/go-acme/lego/v3/log"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/mholt/certmagic"
|
||||
"github.com/rs/cors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Created files are not world writable
|
||||
syscall.Umask(0077)
|
||||
configPtr := flag.String("c", "/etc/acme-dns/config.cfg", "config file location")
|
||||
flag.Parse()
|
||||
// Read global config
|
||||
configTmp := readConfig("config.cfg")
|
||||
DNSConf = configTmp
|
||||
var err error
|
||||
if fileIsAccessible(*configPtr) {
|
||||
log.WithFields(log.Fields{"file": *configPtr}).Info("Using config file")
|
||||
Config, err = readConfig(*configPtr)
|
||||
} else if fileIsAccessible("./config.cfg") {
|
||||
log.WithFields(log.Fields{"file": "./config.cfg"}).Info("Using config file")
|
||||
Config, err = readConfig("./config.cfg")
|
||||
} else {
|
||||
log.Errorf("Configuration file not found.")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorf("Encountered an error while trying to read configuration file: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
setupLogging(DNSConf.Logconfig.Format, DNSConf.Logconfig.Level)
|
||||
|
||||
// Read the default records in
|
||||
RR.Parse(DNSConf.General)
|
||||
setupLogging(Config.Logconfig.Format, Config.Logconfig.Level)
|
||||
|
||||
// Open database
|
||||
newDB := new(acmedb)
|
||||
err := newDB.Init(DNSConf.Database.Engine, DNSConf.Database.Connection)
|
||||
err = newDB.Init(Config.Database.Engine, Config.Database.Connection)
|
||||
if err != nil {
|
||||
log.Errorf("Could not open database [%v]", err)
|
||||
os.Exit(1)
|
||||
} else {
|
||||
log.Info("Connected to database")
|
||||
}
|
||||
DB = newDB
|
||||
defer DB.Close()
|
||||
|
||||
// Error channel for servers
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
// DNS server
|
||||
startDNS(DNSConf.General.Listen, DNSConf.General.Proto)
|
||||
dnsservers := make([]*DNSServer, 0)
|
||||
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(DB, Config.General.Listen, udpProto, Config.General.Domain)
|
||||
dnsservers = append(dnsservers, dnsServerUDP)
|
||||
dnsServerUDP.ParseRecords(Config)
|
||||
dnsServerTCP := NewDNSServer(DB, Config.General.Listen, tcpProto, Config.General.Domain)
|
||||
dnsservers = append(dnsservers, dnsServerTCP)
|
||||
// No need to parse records from config again
|
||||
dnsServerTCP.Domains = dnsServerUDP.Domains
|
||||
dnsServerTCP.SOA = dnsServerUDP.SOA
|
||||
go dnsServerUDP.Start(errChan)
|
||||
go dnsServerTCP.Start(errChan)
|
||||
} else {
|
||||
dnsServer := NewDNSServer(DB, Config.General.Listen, Config.General.Proto, Config.General.Domain)
|
||||
dnsservers = append(dnsservers, dnsServer)
|
||||
dnsServer.ParseRecords(Config)
|
||||
go dnsServer.Start(errChan)
|
||||
}
|
||||
|
||||
// HTTP API
|
||||
startHTTPAPI()
|
||||
go startHTTPAPI(errChan, Config, dnsservers)
|
||||
|
||||
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)
|
||||
// block waiting for error
|
||||
for {
|
||||
err = <-errChan
|
||||
if err != nil {
|
||||
log.Errorf("Error in HTTP server [%v]", err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func startHTTPAPI(errChan chan error, config DNSConfig, dnsservers []*DNSServer) {
|
||||
// Setup http logger
|
||||
logger := log.New()
|
||||
logwriter := logger.Writer()
|
||||
defer logwriter.Close()
|
||||
// Setup logging for different dependencies to log with logrus
|
||||
// Certmagic
|
||||
stdlog.SetOutput(logwriter)
|
||||
// Lego
|
||||
legolog.Logger = logger
|
||||
|
||||
api := httprouter.New()
|
||||
c := cors.New(cors.Options{
|
||||
AllowedOrigins: Config.API.CorsOrigins,
|
||||
AllowedMethods: []string{"GET", "POST"},
|
||||
OptionsPassthrough: false,
|
||||
Debug: Config.General.Debug,
|
||||
})
|
||||
if Config.General.Debug {
|
||||
// Logwriter for saner log output
|
||||
c.Log = stdlog.New(logwriter, "", 0)
|
||||
}
|
||||
if !Config.API.DisableRegistration {
|
||||
api.POST("/register", webRegisterPost)
|
||||
}
|
||||
api.POST("/update", Auth(webUpdatePost))
|
||||
api.GET("/health", healthCheck)
|
||||
|
||||
host := Config.API.IP + ":" + Config.API.Port
|
||||
|
||||
// TLS specific general settings
|
||||
cfg := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
|
||||
provider := NewChallengeProvider(dnsservers)
|
||||
// Override the validation options to mitigate issues with (lack of) 1:1 nat reflection
|
||||
// for some network setups.
|
||||
dnsopts := dns01.WrapPreCheck(func(_, _, _ string, _ dns01.PreCheckFunc) (bool, error) {
|
||||
return true, nil
|
||||
})
|
||||
storage := certmagic.FileStorage{Path: Config.API.ACMECacheDir}
|
||||
magicconf := certmagic.Config{
|
||||
Agreed: true,
|
||||
CA: certmagic.LetsEncryptStagingCA,
|
||||
DNSProvider: &provider,
|
||||
DNSChallengeOption: dnsopts,
|
||||
DefaultServerName: Config.General.Domain,
|
||||
Email: Config.API.NotificationEmail,
|
||||
Storage: &storage,
|
||||
}
|
||||
|
||||
cache := certmagic.NewCache(certmagic.CacheOptions{
|
||||
GetConfigForCert: func(cert certmagic.Certificate) (certmagic.Config, error) {
|
||||
return magicconf, nil
|
||||
},
|
||||
})
|
||||
|
||||
var err error
|
||||
switch Config.API.TLS {
|
||||
case "letsencryptstaging":
|
||||
magicconf.CA = certmagic.LetsEncryptStagingCA
|
||||
certcfg := certmagic.New(cache, magicconf)
|
||||
err = certcfg.ManageSync([]string{Config.General.Domain})
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
cfg.GetCertificate = certcfg.GetCertificate
|
||||
srv := &http.Server{
|
||||
Addr: host,
|
||||
Handler: c.Handler(api),
|
||||
TLSConfig: cfg,
|
||||
ErrorLog: stdlog.New(logwriter, "", 0),
|
||||
}
|
||||
log.WithFields(log.Fields{"host": host, "domain": Config.General.Domain}).Info("Listening HTTPS")
|
||||
err = srv.ListenAndServeTLS("", "")
|
||||
case "letsencrypt":
|
||||
magicconf.CA = certmagic.LetsEncryptProductionCA
|
||||
certcfg := certmagic.New(cache, magicconf)
|
||||
err = certcfg.ManageSync([]string{Config.General.Domain})
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
cfg.GetCertificate = certcfg.GetCertificate
|
||||
srv := &http.Server{
|
||||
Addr: host,
|
||||
Handler: c.Handler(api),
|
||||
TLSConfig: cfg,
|
||||
ErrorLog: stdlog.New(logwriter, "", 0),
|
||||
}
|
||||
log.WithFields(log.Fields{"host": host, "domain": Config.General.Domain}).Info("Listening HTTPS")
|
||||
err = srv.ListenAndServeTLS("", "")
|
||||
case "cert":
|
||||
host := DNSConf.API.Domain + ":" + DNSConf.API.Port
|
||||
api.ListenTLS(host, DNSConf.API.TLSCertFullchain, DNSConf.API.TLSCertPrivkey)
|
||||
srv := &http.Server{
|
||||
Addr: host,
|
||||
Handler: c.Handler(api),
|
||||
TLSConfig: cfg,
|
||||
ErrorLog: stdlog.New(logwriter, "", 0),
|
||||
}
|
||||
log.WithFields(log.Fields{"host": host}).Info("Listening HTTPS")
|
||||
err = srv.ListenAndServeTLS(Config.API.TLSCertFullchain, Config.API.TLSCertPrivkey)
|
||||
default:
|
||||
host := DNSConf.API.Domain + ":" + DNSConf.API.Port
|
||||
api.Listen(host)
|
||||
log.WithFields(log.Fields{"host": host}).Info("Listening HTTP")
|
||||
err = http.ListenAndServe(host, c.Handler(api))
|
||||
}
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
}
|
||||
}
|
||||
|
||||
+22
-8
@@ -3,14 +3,16 @@ package main
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
logrustest "github.com/Sirupsen/logrus/hooks/test"
|
||||
log "github.com/sirupsen/logrus"
|
||||
logrustest "github.com/sirupsen/logrus/hooks/test"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var loghook = new(logrustest.Hook)
|
||||
var dnsserver *DNSServer
|
||||
|
||||
var (
|
||||
postgres = flag.Bool("postgres", false, "run integration tests against PostgreSQL")
|
||||
@@ -19,6 +21,7 @@ var (
|
||||
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",
|
||||
}
|
||||
@@ -26,25 +29,34 @@ var records = []string{
|
||||
func TestMain(m *testing.M) {
|
||||
setupTestLogger()
|
||||
setupConfig()
|
||||
RR.Parse(DNSConf.General)
|
||||
flag.Parse()
|
||||
|
||||
newDb := new(acmedb)
|
||||
if *postgres {
|
||||
DNSConf.Database.Engine = "postgres"
|
||||
Config.Database.Engine = "postgres"
|
||||
err := newDb.Init("postgres", "postgres://acmedns:acmedns@localhost/acmedns")
|
||||
if err != nil {
|
||||
fmt.Println("PostgreSQL integration tests expect database \"acmedns\" running in localhost, with username and password set to \"acmedns\"")
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
DNSConf.Database.Engine = "sqlite3"
|
||||
Config.Database.Engine = "sqlite3"
|
||||
_ = newDb.Init("sqlite3", ":memory:")
|
||||
}
|
||||
DB = newDb
|
||||
server := startDNS("0.0.0.0:15353", "udp")
|
||||
dnsserver = NewDNSServer(DB, Config.General.Listen, Config.General.Proto, Config.General.Domain)
|
||||
dnsserver.ParseRecords(Config)
|
||||
|
||||
// Make sure that we're not creating a race condition in tests
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
dnsserver.Server.NotifyStartedFunc = func() {
|
||||
wg.Done()
|
||||
}
|
||||
go dnsserver.Start(make(chan error, 1))
|
||||
wg.Wait()
|
||||
exitval := m.Run()
|
||||
server.Shutdown()
|
||||
_ = dnsserver.Server.Shutdown()
|
||||
DB.Close()
|
||||
os.Exit(exitval)
|
||||
}
|
||||
@@ -57,6 +69,8 @@ func setupConfig() {
|
||||
|
||||
var generalcfg = general{
|
||||
Domain: "auth.example.org",
|
||||
Listen: "127.0.0.1:15353",
|
||||
Proto: "udp",
|
||||
Nsname: "ns1.auth.example.org",
|
||||
Nsadmin: "admin.example.org",
|
||||
StaticRecords: records,
|
||||
@@ -78,7 +92,7 @@ func setupConfig() {
|
||||
API: httpapicfg,
|
||||
}
|
||||
|
||||
DNSConf = dnscfg
|
||||
Config = dnscfg
|
||||
}
|
||||
|
||||
func setupTestLogger() {
|
||||
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/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
|
||||
@@ -2,25 +2,17 @@ package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/satori/go.uuid"
|
||||
"sync"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// DNSConf is global configuration struct
|
||||
var DNSConf DNSConfig
|
||||
// Config is global configuration struct
|
||||
var Config DNSConfig
|
||||
|
||||
// DB is used to access the database functions in acme-dns
|
||||
var DB database
|
||||
|
||||
// RR holds the static DNS records
|
||||
var RR Records
|
||||
|
||||
// Records is for static records
|
||||
type Records struct {
|
||||
Records map[uint16]map[string][]dns.RR
|
||||
}
|
||||
|
||||
// DNSConfig holds the config structure
|
||||
type DNSConfig struct {
|
||||
General general
|
||||
@@ -29,9 +21,6 @@ type DNSConfig struct {
|
||||
Logconfig logconfig
|
||||
}
|
||||
|
||||
// Auth middleware
|
||||
type authMiddleware struct{}
|
||||
|
||||
// Config file general section
|
||||
type general struct {
|
||||
Listen string
|
||||
@@ -50,14 +39,19 @@ type dbsettings struct {
|
||||
|
||||
// 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"`
|
||||
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
|
||||
@@ -77,8 +71,8 @@ type database interface {
|
||||
Init(string, string) error
|
||||
Register(cidrslice) (ACMETxt, error)
|
||||
GetByUsername(uuid.UUID) (ACMETxt, error)
|
||||
GetByDomain(string) ([]ACMETxt, error)
|
||||
Update(ACMETxt) error
|
||||
GetTXTForDomain(string) ([]string, error)
|
||||
Update(ACMETxtPost) error
|
||||
GetBackend() *sql.DB
|
||||
SetBackend(*sql.DB)
|
||||
Close()
|
||||
|
||||
@@ -2,25 +2,70 @@ package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/miekg/dns"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func readConfig(fname string) DNSConfig {
|
||||
func jsonError(message string) []byte {
|
||||
return []byte(fmt.Sprintf("{\"error\": \"%s\"}", message))
|
||||
}
|
||||
|
||||
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 readConfig(fname string) (DNSConfig, error) {
|
||||
var conf DNSConfig
|
||||
// Practically never errors
|
||||
_, _ = toml.DecodeFile(fname, &conf)
|
||||
return conf
|
||||
_, 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 DNSConfig) (DNSConfig, 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"
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
func sanitizeString(s string) string {
|
||||
// URL safe base64 alphabet without padding as defined in ACME
|
||||
re, _ := regexp.Compile("[^A-Za-z\\-\\_0-9]+")
|
||||
re, _ := regexp.Compile(`[^A-Za-z\-\_0-9]+`)
|
||||
return re.ReplaceAllString(s, "")
|
||||
}
|
||||
|
||||
func sanitizeIPv6addr(s string) string {
|
||||
// Remove brackets from IPv6 addresses, net.ParseCIDR needs this
|
||||
re, _ := regexp.Compile(`[\[\]]+`)
|
||||
return re.ReplaceAllString(s, "")
|
||||
}
|
||||
|
||||
@@ -62,14 +107,6 @@ func setupLogging(format string, level string) {
|
||||
// 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, ",") {
|
||||
|
||||
+56
-2
@@ -1,10 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func TestSetupLogging(t *testing.T) {
|
||||
@@ -62,7 +64,7 @@ func TestReadConfig(t *testing.T) {
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
t.Error("Could not close temporary file")
|
||||
}
|
||||
ret := readConfig(tmpfile.Name())
|
||||
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)
|
||||
}
|
||||
@@ -95,3 +97,55 @@ func TestGetIPListFromHeader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileCheckPermissionDenied(t *testing.T) {
|
||||
tmpfile, err := ioutil.TempFile("", "acmedns")
|
||||
if err != nil {
|
||||
t.Error("Could not create temporary file")
|
||||
}
|
||||
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 := ioutil.TempFile("", "acmedns")
|
||||
if err != nil {
|
||||
t.Error("Could not create temporary file")
|
||||
}
|
||||
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 DNSConfig
|
||||
shoulderror bool
|
||||
}{
|
||||
{DNSConfig{Database: dbsettings{Engine: "whatever", Connection: "whatever_too"}}, false},
|
||||
{DNSConfig{Database: dbsettings{Engine: "", Connection: "whatever_too"}}, true},
|
||||
{DNSConfig{Database: dbsettings{Engine: "whatever", Connection: ""}}, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-7
@@ -2,13 +2,14 @@ package main
|
||||
|
||||
import (
|
||||
"unicode/utf8"
|
||||
"regexp"
|
||||
|
||||
"github.com/satori/go.uuid"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func getValidUsername(u string) (uuid.UUID, error) {
|
||||
uname, err := uuid.FromString(u)
|
||||
uname, err := uuid.Parse(u)
|
||||
if err != nil {
|
||||
return uuid.UUID{}, err
|
||||
}
|
||||
@@ -25,11 +26,9 @@ func validKey(k string) bool {
|
||||
}
|
||||
|
||||
func validSubdomain(s string) bool {
|
||||
_, err := uuid.FromString(s)
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
// 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 {
|
||||
|
||||
+6
-3
@@ -1,12 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/satori/go.uuid"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func TestGetValidUsername(t *testing.T) {
|
||||
v1, _ := uuid.FromString("a097455b-52cc-4569-90c8-7a4b97c6eba8")
|
||||
v1, _ := uuid.Parse("a097455b-52cc-4569-90c8-7a4b97c6eba8")
|
||||
for i, test := range []struct {
|
||||
uname string
|
||||
output uuid.UUID
|
||||
@@ -54,7 +55,9 @@ func TestGetValidSubdomain(t *testing.T) {
|
||||
output bool
|
||||
}{
|
||||
{"a097455b-52cc-4569-90c8-7a4b97c6eba8", true},
|
||||
{"a-97455b-52cc-4569-90c8-7a4b97c6eba8", false},
|
||||
{"a-97455b-52cc-4569-90c8-7a4b97c6eba8", true},
|
||||
{"foo.example.com", false},
|
||||
{"foo-example-com", true},
|
||||
{"", false},
|
||||
{"&!#!25123!%!'%", false},
|
||||
} {
|
||||
|
||||
Vendored
-549
@@ -1,549 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
Reference in New Issue
Block a user