Compare commits

..
119 Commits
Author SHA1 Message Date
Joona Hoikkala cdb1535fa7 Updated Dockerfile to use more recent base image 2022-01-07 16:50:28 +02:00
Joona HoikkalaandGitHub 68bb6ab654 Add acme-dns-client to the documentation (#253) 2021-01-11 18:07:22 +02:00
Joona HoikkalaandGitHub 835fbb9ef6 Migrate to GitHub actions for coverage & unit test automation (#251) 2021-01-11 17:31:09 +02:00
Jonathan VanascoandGitHub 9c6ca258e1 relax subdomain validation from UUID to actual subdomain (#243) 2021-01-11 14:55:31 +02:00
Peter LebbingandGitHub d573f771a8 README: Use https URLs for API testing (#230) 2021-01-11 14:50:48 +02:00
Brett KellerandGitHub 035a219f9f Add support for LE expiration notification e-mails on API cert (#227) 2021-01-11 14:35:54 +02:00
Wouter TinusandGitHub 19069f50ec Update README.md (#210)
Add new website for win-acme
2020-02-10 19:27:16 +02:00
Joona HoikkalaandGitHub 5de21916a3 Handle go modules properly in Dockerfile with Go 1.13 (#207) 2020-01-10 12:32:11 +02:00
Joona HoikkalaandGitHub 3d5a512d1e Revert "Update modules (#203)" (#206)
This reverts commit 1681de11d2.
2020-01-09 08:39:04 +02:00
Joona HoikkalaandGitHub 9f7a158367 Update readme (#204) 2020-01-08 20:40:37 +02:00
Joona HoikkalaandGitHub 1681de11d2 Update modules (#203) 2020-01-08 07:53:11 +02:00
Marvin BlumandJoona Hoikkala 88d3be685e Removed example for health check endpoint because it is misleading. (#181) 2019-10-21 16:13:22 +03:00
Joona HoikkalaandGitHub aa3e7e1b75 Migrate over to certmagic from using autocert (#190)
* Use certmagic for challenge validation

* WIP

* Get the correct key

* Override preflight check logic

* Fix logging for imported packages and tidy config.cfg

* Fix test and add docstrings

* Update README
2019-10-20 22:52:19 +03:00
Joona HoikkalaandGitHub af542b44a9 Proper EDNS0 (non)support (#188)
* Proper EDNS0 (non)support

* Add changelog entry

* Add EDNS0 tests
2019-10-18 22:24:56 +03:00
Joona HoikkalaandGitHub 5b1e51534f Migrate to Go 1.13 and gomodules (#187) 2019-10-17 14:21:04 +03:00
lateagainandJoona Hoikkala 637c82f407 Update acme-dns.service (#183)
Set working directory to service users home.
2019-09-18 16:05:32 +03:00
Daniel McCarneyandJoona Hoikkala e1f1d6af34 README: Add warning/advice about HTTPS API. (#169)
This commit updates the README to add a brief description of the `tls`
configuration options. In particular using `tls = "letsencrypt"` is
recommended and a warning is added about using `tls = "cert"` and
allowing the certificate to expire.
2019-06-12 15:42:23 +03:00
znerolandJoona Hoikkala c13035a344 Refactor: Use more specific type in argument of DB.Update (#162)
The DB.Update function takes a type of ACMETxt. However, the function
only requires the Value and Subdomain fields.

Refactor the function such that it takes ACMETxtPost instead of the full
ACMETxt record. This will simplify extraction of txt-record related
logic from the db code.
2019-06-12 15:41:02 +03:00
Joona HoikkalaandGitHub af5d2561d2 Fail closed with malformed allowfrom data in register endpoint (#148)
* Prepare readme for release

* Fail closed with malformed allowfrom data in register endpoint
2019-02-22 16:53:11 +02:00
Daniel Lo NigroandJoona Hoikkala 395cb7a62c Add Windows ACME Simple (win-acme) to clients list (#134)
* Add win-acme to clients list

* Use its official name ("Windows ACME Simple")

* Remove version number from WACS
2019-02-22 16:42:52 +02:00
Joona HoikkalaandGitHub 5d74ff1142 Prepare README for point release v0.7.2 (#153) 2019-02-07 09:58:07 +02:00
Ward VandewegeandJoona Hoikkala 37db83e5b7 Respond case insensitively to A and SOA requests (#152)
* When appending the SOA for authoritative NXDOMAIN responses, it needs to go in
the Authoritative section, not the Answer section.

This fixes the acme-dns validation for the lego Let's Encrypt client.

* Respond case-insensitively to A and SOA requests. Add corresponding tests.

This fixes the autocert feature with Let's Encrypt, because Let's Encrypt does
a lookup for the A record with a deliberately mangled case.
2019-02-07 09:16:33 +02:00
Ward VandewegeandJoona Hoikkala 41a1cff0ae When appending the SOA for authoritative NXDOMAIN responses, it needs to go in (#151)
the Authoritative section, not the Answer section.

This fixes the acme-dns validation for the lego Let's Encrypt client.
2019-02-07 09:13:47 +02:00
Joona HoikkalaandGitHub 4f5fad0d32 Prepare for point release (#150)
* Prepare for point release
2019-02-06 12:13:00 +02:00
Ward VandewegeandJoona Hoikkala 5651772837 Make sure to initialize the SOA record for the tcp server, too. (#149) 2019-02-06 12:09:01 +02:00
Joona HoikkalaandGitHub 09dc25d336 Update vendored dependencies (#147)
* Prepare readme for release

* Update vendored packages

* New version of dns dependency handles rejecting UPDATEs under the hood

* Go 1.11 required
2019-02-04 00:21:42 +02:00
Joona HoikkalaandGitHub a88ee29755 Prepare readme for release (#146) 2019-02-03 20:32:02 +02:00
Joona HoikkalaandGitHub de7fe3cb1d Fix responses to be more standards compliant (#145)
* Handle OPT questions (EDNS)

* Handle authoritative bit, and append SOA for authoritative NXDOMAIN responses

* Changelog entry

* Fix linter errors
2019-02-03 20:19:15 +02:00
Joona HoikkalaandGitHub 7a2f9f06b1 Refactoring DNS server part for safer paraller execution (#144)
* Refactoring DNS server part for safer paraller execution and better data structures

* Fix linter issues
2019-02-03 17:23:04 +02:00
Joona HoikkalaandGitHub d695f72963 Fix the default configuration SQLite db path (#143) 2019-02-01 09:55:26 +02:00
Marvin BlumandJoona Hoikkala aff13a02fb Added http health check endpoint. (#137)
* Added http health check endpoint.

* Fixed performing POST on GET endpoint.

* Explicitly return http status 200 in health check endpoint.

* Updated changelog.
2019-01-25 19:22:53 +02:00
Joona HoikkalaandGitHub 7fbb5261c8 Fix test not to assume preset UseHeaders value (#139) 2019-01-25 18:36:29 +02:00
Philip VieiraandJoona Hoikkala 2da94f1462 Clarify how to test that the DNS Lookup is working (#133)
Reasoning: Unless you specify that you're looking for a TXT record, you will not get the answer from the DNS server
2019-01-20 18:23:07 +02:00
Christopher CookandJoona Hoikkala ea4d125663 Add Certify The Web to clients list (#131)
Certify now supports acme-dns in the latest beta.
https://github.com/webprofusion/certify/issues/391
2018-12-18 11:05:01 +02:00
Joona HoikkalaandGitHub f64de0353d Fix Docker instructions and add option to bind both UDP and TCP DNS listeners (#130) 2018-12-13 12:19:10 +02:00
Joona HoikkalaandGitHub 20411b650f Update changelog and prepare for release v0.6 (#125) 2018-10-31 15:13:29 +02:00
Joona HoikkalaandGitHub 3bb130d055 Refuse dynamic updates properly (#124) 2018-10-31 15:01:42 +02:00
Joona HoikkalaandGitHub 1d85bf75b0 Added Traefik and cert-manager to client list (#123) 2018-10-31 01:13:10 +02:00
Joona HoikkalaandGitHub c2c5c5cd70 Better error handling in goroutines (#122)
* More robust goroutine error handling using channels

* Fix tests and make startup log msg saner

* Clarification to README and config file
2018-10-31 00:54:51 +02:00
golint fixerandJoona Hoikkala a09073da12 Fix golint import path (#121) 2018-10-29 10:29:34 +02:00
Julien SchmidtandJoona Hoikkala 4bb5e265ab README: use absolute path for sqlite DB in example config (#116) 2018-09-28 11:40:50 +03:00
Julien SchmidtandJoona Hoikkala b452d504c8 README: fix NS address in test command (#115) 2018-09-28 11:14:31 +03:00
Yannik SembritzkiandJoona Hoikkala f76790426d fix inconsistency between README and actual config (#114)
* fix inconsistency between README and actual config

* clarify example config in sync w/ config.cfg
2018-09-28 00:46:07 +03:00
Yannik SembritzkiandJoona Hoikkala f650e47fe5 fix README inconsistencies (#113) 2018-09-28 00:33:51 +03:00
Yannik SembritzkiandJoona Hoikkala dc0dd43017 simplify example dns config (#106) 2018-09-27 23:58:37 +03:00
Julien SchmidtandJoona Hoikkala c827ee4801 README: fix section of HTTP header vars in example config (#112) 2018-09-27 23:51:31 +03:00
Stefan BethkeandJoona Hoikkala db2a6bc288 Cmd line flag -c for config location (#108)
Add command line parsing and a flag `-c` to specify where the config
file should be loaded from.
2018-09-21 13:38:23 +03:00
Joona HoikkalaandGitHub d66ccffaf6 Sign releases (#105) 2018-08-12 21:04:07 +03:00
Joona HoikkalaandGitHub eeff02e63b Changelog entries for v0.5 (#104) 2018-08-12 20:51:04 +03:00
Joona HoikkalaandGitHub 90ae6ee268 Default value for added configuration option to keep backwards compatibility with old config (#103) 2018-08-12 20:19:58 +03:00
Joona HoikkalaandGitHub 0fc5a8e848 Use umask 0077 across the process in order to have the created files readable only by the acme-dns user (#102) 2018-08-12 20:06:54 +03:00
Joona HoikkalaandGitHub ec013c0f25 Better error messages for missing DB config values (#101)
* Better error messages for missing DB config values

* Make linter happy
2018-08-12 19:48:39 +03:00
Joona HoikkalaandGitHub 856cc05881 Added supplementary error checking (#99)
* Added supplementary errorichecking

* After running util.go through gofmt

* Updated main and util

* Minor updates to main and util

* Slight refactoring

* Add tests
2018-08-12 18:49:17 +03:00
Joona HoikkalaandGitHub 75d4a30c1f Update dependencies and replace uuid library (#100) 2018-08-10 16:51:32 +03:00
Daniel McCarneyandJoona Hoikkala 8aa869b2f8 README: Add Lego to supported clients (#94)
* README: Add Lego to supported clients

* README: List clients in alpha sort
2018-07-10 00:28:53 +03:00
Daniel McCarneyandJoona Hoikkala 52e977ce1d README: Add goacmedns, a Go acme-dns client library to README. (#92) 2018-07-01 22:54:37 +03:00
Josh SorefandJoona Hoikkala 0bbbf5ed39 Minor (#90)
* add periods to sentences

* markdown

* reword

* reword: the item _acme-challenge should be a cname, not a cname to _acme-challenge
2018-06-05 11:50:42 +03:00
Josh SorefandJoona Hoikkala fbd5c65a6a Spelling (#89)
* spelling: the challenging

* spelling: effects

* spelling: received

* spelling: vagrantfile
2018-06-05 11:42:16 +03:00
Gabe CookandJoona Hoikkala 4646e7f7e5 Add systemd service information (#83)
* Add systemd service information

* Requested changes for systemd service information
2018-05-27 00:08:55 +03:00
Andrew MeyerandJoona Hoikkala 3006cb712b Make ACME cache directory location configurable (#81)
* Remove trailing whitespace from README and config

* Make ACME cache directory location configurable
2018-05-14 13:42:39 +03:00
Ryan BolgerandJoona Hoikkala fde566fe67 Added Posh-ACME to client list (#78)
* Added Posh-ACME to client list

* alphabetical order as requested
2018-05-02 23:15:50 +03:00
Joona HoikkalaandGitHub a17792d33c Merge pull request #75 from joohoi/golintfix-revert
Revert "Disable golint until golang/go#25048 is resolved."
2018-04-25 11:50:32 +03:00
Joona Hoikkala d1af1d029f Revert "Disable golint until golang/go#25048 is resolved."
This reverts commit 1c918fcaa4.
2018-04-25 11:44:34 +03:00
Joona HoikkalaandGitHub a5b417901c Merge pull request #73 from joohoi/add-sewer
README: Add Sewer client
2018-04-24 15:00:30 +03:00
Joona Hoikkala 065083781e Merge remote-tracking branch 'origin/master' into add-sewer 2018-04-24 14:55:32 +03:00
Joona HoikkalaandGitHub 0af5dc2cd9 Merge pull request #74 from joohoi/golintfix
Fix golint dependency issue
2018-04-24 14:55:05 +03:00
Joona Hoikkala 1c918fcaa4 Disable golint until golang/go#25048 is resolved. 2018-04-24 14:49:20 +03:00
Joona Hoikkala 299c520c2b Add Sewer client 2018-04-24 14:17:59 +03:00
Joona HoikkalaandGitHub 3343d943d6 Merge pull request #68 from joohoi/update-clients
README: add acme.sh and change Certbot hook URL
2018-04-08 12:55:59 +03:00
Joona Hoikkala 7744357b61 README: add acme.sh and change Certbot hook URL 2018-04-08 12:51:06 +03:00
Joona HoikkalaandGitHub 14f552e970 Merge pull request #66 from cpu/cpu-typo-fix
db_test: fix 'recieve' typo
2018-04-08 12:41:20 +03:00
Joona HoikkalaandGitHub f2d1fc692b Merge branch 'master' into cpu-typo-fix 2018-04-08 12:37:14 +03:00
Joona HoikkalaandGitHub dd12fa7c9b Merge pull request #64 from Daniel15/patch-2
Make installation instructions more comprehensive
2018-04-08 12:34:00 +03:00
Joona HoikkalaandGitHub 0991b3e3c9 Merge branch 'master' into patch-2 2018-04-08 12:24:24 +03:00
Daniel d18cd65374 db_test: fix 'recieve' typo 2018-03-26 17:22:23 -04:00
Daniel Lo NigroandGitHub 8eed75b495 Explain CNAMEs in DNS records section 2018-03-22 20:37:07 -07:00
Daniel Lo NigroandGitHub 46b9c45e1b Remove unintended newline 2018-03-22 20:33:30 -07:00
Daniel Lo NigroandGitHub ed82e15349 Refactor readme changes
- Move DNS config to separate "DNS Records" section
- Move testing to separate "Testing It Out" section
2018-03-22 20:32:56 -07:00
Joona HoikkalaandGitHub 7b2203ccca Merge pull request #65 from joohoi/clients
README: Add link to a client hook written in Go
2018-03-20 15:01:51 +02:00
Joona Hoikkala 945013579f README: Add link to a client hook written in Go 2018-03-20 14:56:35 +02:00
Daniel Lo NigroandGitHub 3ee50d0c20 Make installation instructions more comprehensive 2018-03-18 23:47:45 -07:00
Joona HoikkalaandGitHub f2fe21934c Merge pull request #60 from joohoi/link_clientlib
README: Add link to the client library
2018-03-18 17:13:23 +02:00
Joona Hoikkala 07aa5e0043 README: Add link to the client library 2018-03-18 16:02:25 +02:00
Joona HoikkalaandGitHub 01f010e35e README: link to client implementation (#55) 2018-03-15 13:40:45 +02:00
Joona HoikkalaandGitHub cd3a587e43 Release v0.4 (#53) 2018-03-15 00:47:36 +02:00
Joona HoikkalaandGitHub 439da9c09f Properly parse r.RemoteAddr (#50)
* Properly parse r.RemoteAddr

* Add tests, and fix net.ParseCIDR issues with IPv6 addresses enclosed in brackets
2018-03-15 00:23:55 +02:00
Joona HoikkalaandGitHub 5c2e60a828 Add configuration option to disable registration endpoint (#51) 2018-03-14 23:35:39 +02:00
Joona HoikkalaandGitHub d542ee03b5 Merge pull request #42 from cpu/cpu-differentiate-errors
API: Differentiate bad TXT update error.
2018-03-13 11:32:40 +02:00
Joona HoikkalaandGitHub 92f8cc2802 Merge branch 'master' into cpu-differentiate-errors 2018-03-13 10:55:02 +02:00
Joona HoikkalaandGitHub 978ac5d62b Merge pull request #49 from Yannik/patch-1
return cname if requested RR was not found and cname exists (fixes #48)
2018-03-13 10:42:44 +02:00
Yannik Sembritzki 4d214d7f52 return cname if requested RR was not found and cname exists 2018-03-03 11:24:32 +01:00
Daniel McCarneyandGitHub f463d07d0b Merge branch 'master' into cpu-differentiate-errors 2018-03-01 09:54:41 -05:00
Joona HoikkalaandGitHub 9a908d7d6b Log IP address that we're matching against allowFrom values stored in the DB (#46)
* Add logging for IP matching

* Fix typo
2018-03-01 16:53:38 +02:00
Daniel McCarneyandJoona Hoikkala 11c852ee91 README: simplify installation instructions with go get. (#41)
* README: simplify installation instructions with `go get`.

Prior to this commit the installation instructions in the README had you
use `git clone` and then `go build`. This can be collapsed into one step
using a more idiomatic `go get` command. This commit updates the README
accordingly.

* Also mention supported config file locations

* Fix go get URL
2018-02-28 20:38:44 +02:00
Joona HoikkalaandGitHub 32608e9f47 Merge pull request #44 from cpu/cpu-add-api-ip-example-config
README: Add example 'ip' in example API config.
2018-02-28 20:24:08 +02:00
Daniel 77b5fda6fb README: Add example 'ip' in example API config.
`acme-dns` supports binding the API to a specific interface instead of
all interfaces by providing an `ip` address in the `[api]` configuration
section. Prior to this commit the `ip` field wasn't shown in the example
configuration in the README. This commit adds an example showing how to
configure `acme-dns` to listen only on `127.0.0.1` to the example config
and describes what the default value (`""`) does.
2018-02-27 21:45:13 -05:00
Daniel efdd560ee4 API: Differentiate bad TXT update error.
Previous to this commit, if the update message had a valid subdomain but
an invalid TXT the error returned was for a bad subdomain. This can
confuse developers who were POSTing junk TXT records to test acme-dns
:-)

This commit adjusts the `webUpdatePost` error handling such that
`!validSubdomain(input)` and `!validTXT(input)` give distinct errors.

The `!validSubdomain` case should never happen in `webUpdatePost`
because `auth.go`'s `Auth` function already vets the post data
subdomain but I retained the error handling code just in case.

Unit tests for an update with an invalid subdomain and an update with an
invalid TXT are included.
2018-02-27 19:26:15 -05:00
Joona HoikkalaandGitHub 830cceb62c Release v0.3.2 (#40) 2018-02-01 16:10:24 +02:00
Leon KyneurandJoona Hoikkala 9f896759f0 reinstall ca-certificates (#39) 2018-02-01 16:02:06 +02:00
Joona HoikkalaandGitHub 476f6fc555 Only autobuild for linux, as sqlite needs cgo (#38) 2018-02-01 15:33:57 +02:00
Joona HoikkalaandGitHub e2e84ede6b Goreleaser config (#37) 2018-02-01 14:20:40 +02:00
Joona HoikkalaandGitHub 562d7cbad4 Make autocert use HTTP-01 challenge instead of TLS-SNI (#36) 2018-02-01 10:53:34 +02:00
Joona HoikkalaandGitHub 5470ba7a41 Remove unused variable and prepare for v0.2 (#22)
* Remove unused variable and prepare for v0.2

* Added new changes to changelog and feature list

* Modified changelog
2018-01-22 12:47:26 +02:00
Joona HoikkalaandGitHub 665455d319 Docker instructions and configuration (#33)
* Added dockerfile

* Docker configuration

* Added Docker images, composer configuration and documentation
2018-01-22 12:35:07 +02:00
Joona HoikkalaandGitHub c5337fc841 Update dependencies (#32) 2018-01-22 11:19:33 +02:00
Joona HoikkalaandGitHub 733245fb3d Support for multiple TXT records per subdomain (#29)
* Support for multiple TXT records per subdomain and database upgrade functionality

* Linter fixes

* Make sure the database upgrade routine works for PostgreSQL

* Move subdomain query outside of the upgrade transaction
2018-01-22 09:53:07 +02:00
Joona HoikkalaandGitHub ba695134ce Make cors messages respect the debug flag (#25) 2017-11-15 21:35:35 +02:00
Joona HoikkalaandGitHub b0cd264c71 Fail on malformed JSON payloads in register endpoint (#24) 2017-11-15 13:52:27 +02:00
Joona HoikkalaandGitHub 02d42bff30 Removed unnecessary body check (#21) 2017-11-15 01:39:00 +02:00
Joona HoikkalaandGitHub fd9ce4606d Get rid of Iris and use julienschmidt/httprouter instead (#20)
* Replace iris with httprouter

* Linter fixes

* Finalize iris removal

* Vendor dependencies for reproducable builds

* Api tests are back
2017-11-14 23:54:29 +02:00
Koen VlaswinkelandJoona Hoikkala 93871a7cec Fix config not being used (#19) 2017-11-14 13:01:46 +02:00
Joona HoikkalaandGitHub 9c54da3ee6 Try to read config from under /etc (#18) 2017-11-14 00:42:30 +02:00
Joona HoikkalaandGitHub 9c639223ce Added dep vendoring (#17) 2017-11-12 23:57:45 +02:00
Joona HoikkalaandGitHub c70a6cffb0 Go 1.9 and Iris v8, added possibility to bind to IP (#15) 2017-11-12 23:40:15 +02:00
Daniel McCarneyandJoona Hoikkala 0ec12dbc5f Update README json examples. (#12)
This commit fixes the JSON examples in the README. There was a missing
`]` in the example input, a missing IP range in the reflected output,
and a few trailing `,` that shouldn't have been there.
2017-09-01 09:37:22 +03:00
Joona HoikkalaandGitHub 41b2ff5940 Use iris v6 and Go 1.8+ (#10)
* Quick fixes to bring framework up to date

* Script for test running, api tests need complete rewrite

* Removed govendor from tests

* Fix for AutoTLS
2017-08-02 19:25:27 +03:00
Joona HoikkalaandGitHub 2bfeedda4c Fixed iris version to v5 api (#9) 2017-01-30 12:19:22 +02:00
Joona Hoikkala 872e2b7c6f Service offline 2017-01-30 09:32:01 +02:00
Joona HoikkalaandGitHub 220ef6d3c0 Merge pull request #7 from joohoi/release-0.1
Release 0.1
2016-12-05 00:43:10 +02:00
32 changed files with 2237 additions and 1083 deletions
+33
View File
@@ -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
+25
View File
@@ -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
View File
@@ -3,5 +3,4 @@ acme-dns.db
acme-dns.log
.vagrant
coverage.out
vendor/*/
/vendor/**/.git
.idea/
+21
View File
@@ -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
View File
@@ -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
View File
@@ -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
+246 -54
View File
@@ -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).
[![asciicast](https://asciinema.org/a/94903.png)](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
View File
@@ -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"
+14
View File
@@ -0,0 +1,14 @@
[Unit]
Description=Limited DNS server with RESTful HTTP API to handle ACME DNS challenges easily and securely
After=network.target
[Service]
User=acme-dns
Group=acme-dns
AmbientCapabilities=CAP_NET_BIND_SERVICE
WorkingDirectory=~
ExecStart=/usr/local/bin/acme-dns
Restart=on-failure
[Install]
WantedBy=multi-user.target
+22 -7
View File
@@ -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
}
+86 -81
View File
@@ -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
View File
@@ -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)
}
+93
View File
@@ -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)
}
+34
View File
@@ -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)
}
}
}
+30
View File
@@ -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
View File
@@ -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"
+170 -39
View File
@@ -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
View File
@@ -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)
}
}
+211 -72
View File
@@ -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
View File
@@ -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")
}
}
+15
View File
@@ -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
+38
View File
@@ -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
)
+393
View File
@@ -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=
+182 -37
View File
@@ -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
View File
@@ -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
View File
@@ -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
+19 -25
View File
@@ -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()
+52 -15
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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},
} {
-549
View File
@@ -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"
}