├── .github └── workflows │ ├── go_cov.yml │ └── golangci-lint.yml ├── .gitignore ├── .goreleaser.yml ├── Dockerfile ├── LICENSE ├── README.md ├── Vagrantfile ├── acme-dns.service ├── acmetxt.go ├── api.go ├── api_test.go ├── auth.go ├── auth_test.go ├── challengeprovider.go ├── config.cfg ├── db.go ├── db_test.go ├── dns.go ├── dns_test.go ├── docker-compose.yml ├── go.mod ├── go.sum ├── main.go ├── main_test.go ├── run_tests.sh ├── test ├── pgsql.sh └── run_integration.sh ├── types.go ├── util.go ├── util_test.go ├── validation.go └── validation_test.go /.github/workflows/go_cov.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: 3 | push: 4 | pull_request: 5 | schedule: 6 | # Run every 12 hours, at the 15 minute mark. E.g. 7 | # 2020-11-29 00:15:00 UTC, 2020-11-29 12:15:00 UTC, 2020-11-30 00:15:00 UTC 8 | - cron: '15 */12 * * *' 9 | jobs: 10 | 11 | build: 12 | name: Build and Unit Test 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ^1.13 20 | 21 | - name: Check out code 22 | uses: actions/checkout@v2 23 | 24 | - name: Build 25 | run: go build -v ./... 26 | 27 | - name: Test 28 | run: go test -v -race -covermode=atomic -coverprofile=coverage.out ./... 29 | 30 | - name: Upload Coverage 31 | uses: shogo82148/actions-goveralls@v1 32 | with: 33 | path-to-profile: coverage.out 34 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - main 8 | - master 9 | pull_request: 10 | 11 | permissions: 12 | contents: read 13 | # Optional: allow read access to pull request. Use with `only-new-issues` option. 14 | # pull-requests: read 15 | 16 | jobs: 17 | golangci: 18 | name: lint 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: actions/setup-go@v5 23 | with: 24 | go-version: stable 25 | - name: golangci-lint 26 | uses: golangci/golangci-lint-action@v6 27 | with: 28 | version: v1.60 29 | args: --timeout=10m 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | acme-dns 2 | acme-dns.db 3 | acme-dns.log 4 | .vagrant 5 | coverage.out 6 | .idea/ 7 | dist/ 8 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - binary: acme-dns 3 | env: 4 | - CGO_ENABLED=1 5 | goos: 6 | - linux 7 | goarch: 8 | - amd64 9 | 10 | archives: 11 | - id: tgz 12 | format: tar.gz 13 | files: 14 | - LICENSE 15 | - README.md 16 | - Dockerfile 17 | - config.cfg 18 | - acme-dns.service 19 | 20 | signs: 21 | - artifacts: checksum 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS builder 2 | LABEL maintainer="joona@kuori.org" 3 | 4 | RUN apk add --update gcc musl-dev git 5 | 6 | ENV GOPATH /tmp/buildcache 7 | RUN git clone https://github.com/joohoi/acme-dns /tmp/acme-dns 8 | WORKDIR /tmp/acme-dns 9 | RUN CGO_ENABLED=1 go build 10 | 11 | FROM alpine:latest 12 | 13 | WORKDIR /root/ 14 | COPY --from=builder /tmp/acme-dns . 15 | RUN mkdir -p /etc/acme-dns 16 | RUN mkdir -p /var/lib/acme-dns 17 | RUN rm -rf ./config.cfg 18 | RUN apk --no-cache add ca-certificates && update-ca-certificates 19 | 20 | VOLUME ["/etc/acme-dns", "/var/lib/acme-dns"] 21 | ENTRYPOINT ["./acme-dns"] 22 | EXPOSE 53 80 443 23 | EXPOSE 53/udp 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Joona Hoikkala 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/joohoi/acme-dns.svg?branch=master)](https://travis-ci.org/joohoi/acme-dns) [![Coverage Status](https://coveralls.io/repos/github/joohoi/acme-dns/badge.svg?branch=master)](https://coveralls.io/github/joohoi/acme-dns?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/joohoi/acme-dns)](https://goreportcard.com/report/github.com/joohoi/acme-dns) 2 | # acme-dns 3 | 4 | A simplified DNS server with a RESTful HTTP API to provide a simple way to automate ACME DNS challenges. 5 | 6 | ## Why? 7 | 8 | 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. 9 | Leaving the keys laying around your random boxes is too often a requirement to have a meaningful process automation. 10 | 11 | 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. 12 | 13 | So basically it boils down to **accessibility** and **security**. 14 | 15 | 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 16 | 17 | ## Features 18 | - Simplified DNS server, serving your ACME DNS challenges (TXT) 19 | - Custom records (have your required A, AAAA, NS, etc. records served) 20 | - HTTP API automatically acquires and uses Let's Encrypt TLS certificate 21 | - Limit /update API endpoint access to specific CIDR mask(s), defined in the /register request 22 | - Supports SQLite & PostgreSQL as DB backends 23 | - 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. 24 | - Simple deployment (it's Go after all) 25 | 26 | ## Usage 27 | 28 | 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). 29 | 30 | [![asciicast](https://asciinema.org/a/94903.png)](https://asciinema.org/a/94903) 31 | 32 | Using acme-dns is a three-step process (provided you already have the self-hosted server set up): 33 | 34 | - Get credentials and unique subdomain (simple POST request to eg. https://auth.acme-dns.io/register) 35 | - 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` ) 36 | - Use your credentials to POST new DNS challenge values to an acme-dns server for the CA to validate from. 37 | - Crontab and forget. 38 | 39 | ## API 40 | 41 | ### Register endpoint 42 | 43 | The method returns a new unique subdomain and credentials needed to update your record. 44 | Fulldomain is where you can point your own `_acme-challenge` subdomain CNAME record to. 45 | 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. 46 | 47 | **Optional:**: You can POST JSON data to limit the `/update` requests to predefined source networks using CIDR notation. 48 | 49 | ```POST /register``` 50 | 51 | #### OPTIONAL Example input 52 | ```json 53 | { 54 | "allowfrom": [ 55 | "192.168.100.1/24", 56 | "1.2.3.4/32", 57 | "2002:c0a8:2a00::0/40" 58 | ] 59 | } 60 | ``` 61 | 62 | 63 | ```Status: 201 Created``` 64 | ```json 65 | { 66 | "allowfrom": [ 67 | "192.168.100.1/24", 68 | "1.2.3.4/32", 69 | "2002:c0a8:2a00::0/40" 70 | ], 71 | "fulldomain": "8e5700ea-a4bf-41c7-8a77-e990661dcc6a.auth.acme-dns.io", 72 | "password": "htB9mR9DYgcu9bX_afHF62erXaH2TS7bg9KW3F7Z", 73 | "subdomain": "8e5700ea-a4bf-41c7-8a77-e990661dcc6a", 74 | "username": "c36f50e8-4632-44f0-83fe-e070fef28a10" 75 | } 76 | ``` 77 | 78 | ### Update endpoint 79 | 80 | The method allows you to update the TXT answer contents of your unique subdomain. Usually carried automatically by automated ACME client. 81 | 82 | ```POST /update``` 83 | 84 | #### Required headers 85 | | Header name | Description | Example | 86 | | ------------- |--------------------------------------------|-------------------------------------------------------| 87 | | X-Api-User | UUIDv4 username received from registration | `X-Api-User: c36f50e8-4632-44f0-83fe-e070fef28a10` | 88 | | X-Api-Key | Password received from registration | `X-Api-Key: htB9mR9DYgcu9bX_afHF62erXaH2TS7bg9KW3F7Z` | 89 | 90 | #### Example input 91 | ```json 92 | { 93 | "subdomain": "8e5700ea-a4bf-41c7-8a77-e990661dcc6a", 94 | "txt": "___validation_token_received_from_the_ca___" 95 | } 96 | ``` 97 | 98 | #### Response 99 | 100 | ```Status: 200 OK``` 101 | ```json 102 | { 103 | "txt": "___validation_token_received_from_the_ca___" 104 | } 105 | ``` 106 | 107 | ### Health check endpoint 108 | 109 | 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. 110 | 111 | ```GET /health``` 112 | 113 | ## Self-hosted 114 | 115 | 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. 116 | 117 | See the INSTALL section for information on how to do this. 118 | 119 | 120 | ## Installation 121 | 122 | 1) Install [Go 1.13 or newer](https://golang.org/doc/install). 123 | 124 | 2) Build acme-dns: 125 | ``` 126 | git clone https://github.com/joohoi/acme-dns 127 | cd acme-dns 128 | export GOPATH=/tmp/acme-dns 129 | go build 130 | ``` 131 | 132 | 3) Move the built acme-dns binary to a directory in your $PATH, for example: 133 | `sudo mv acme-dns /usr/local/bin` 134 | 135 | 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. 136 | 137 | 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. 138 | 139 | 1) Make sure that you have moved the configuration file to `/etc/acme-dns/config.cfg` so that acme-dns can access it globally. 140 | 141 | 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). 142 | 143 | 3) Create a minimal acme-dns user: `sudo adduser --system --gecos "acme-dns Service" --disabled-password --group --home /var/lib/acme-dns acme-dns`. 144 | 145 | 4) Move the systemd service unit from `acme-dns.service` to `/etc/systemd/system/acme-dns.service`. 146 | 147 | 5) Reload systemd units: `sudo systemctl daemon-reload`. 148 | 149 | 6) Enable acme-dns on boot: `sudo systemctl enable acme-dns.service`. 150 | 151 | 7) Run acme-dns: `sudo systemctl start acme-dns.service`. 152 | 153 | 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. 154 | 155 | ### Using Docker 156 | 157 | 1) Pull the latest acme-dns Docker image: `docker pull joohoi/acme-dns`. 158 | 159 | 2) Create directories: `config` for the configuration file, and `data` for the sqlite3 database. 160 | 161 | 3) Copy [configuration template](https://raw.githubusercontent.com/joohoi/acme-dns/master/config.cfg) to `config/config.cfg`. 162 | 163 | 4) Modify the `config.cfg` to suit your needs. 164 | 165 | 5) Run Docker, this example expects that you have `port = "80"` in your `config.cfg`: 166 | ``` 167 | docker run --rm --name acmedns \ 168 | -p 53:53 \ 169 | -p 53:53/udp \ 170 | -p 80:80 \ 171 | -v /path/to/your/config:/etc/acme-dns:ro \ 172 | -v /path/to/your/data:/var/lib/acme-dns \ 173 | -d joohoi/acme-dns 174 | ``` 175 | 176 | ### Docker Compose 177 | 178 | 1) Create directories: `config` for the configuration file, and `data` for the sqlite3 database. 179 | 180 | 2) Copy [configuration template](https://raw.githubusercontent.com/joohoi/acme-dns/master/config.cfg) to `config/config.cfg`. 181 | 182 | 3) Copy [docker-compose.yml from the project](https://raw.githubusercontent.com/joohoi/acme-dns/master/docker-compose.yml), or create your own. 183 | 184 | 4) Edit the `config/config.cfg` and `docker-compose.yml` to suit your needs, and run `docker-compose up -d`. 185 | 186 | ## DNS Records 187 | 188 | Note: In this documentation: 189 | - `auth.example.org` is the hostname of the acme-dns server 190 | - acme-dns will serve `*.auth.example.org` records 191 | - `198.51.100.1` is the **public** IP address of the system running acme-dns 192 | 193 | These values should be changed based on your environment. 194 | 195 | You will need to add some DNS records on your domain's regular DNS server: 196 | - `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) 197 | - `A` record for `auth.example.org` pointing to `198.51.100.1` 198 | - If using IPv6, an `AAAA` record pointing to the IPv6 address. 199 | - 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. 200 | 201 | ## Testing It Out 202 | 203 | You may want to test that acme-dns is working before using it for real queries. 204 | 205 | 1) Confirm that DNS lookups for the acme-dns subdomain works as expected: `dig auth.example.org`. 206 | 207 | 2) Call the `/register` API endpoint to register a test domain: 208 | ``` 209 | $ curl -X POST https://auth.example.org/register 210 | {"username":"eabcdb41-d89f-4580-826f-3e62e9755ef2","password":"pbAXVjlIOE01xbut7YnAbkhMQIkcwoHO0ek2j4Q0","fulldomain":"d420c923-bbd7-4056-ab64-c3ca54c9b3cf.auth.example.org","subdomain":"d420c923-bbd7-4056-ab64-c3ca54c9b3cf","allowfrom":[]} 211 | ``` 212 | 213 | 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: 214 | ``` 215 | $ curl -X POST \ 216 | -H "X-Api-User: eabcdb41-d89f-4580-826f-3e62e9755ef2" \ 217 | -H "X-Api-Key: pbAXVjlIOE01xbut7YnAbkhMQIkcwoHO0ek2j4Q0" \ 218 | -d '{"subdomain": "d420c923-bbd7-4056-ab64-c3ca54c9b3cf", "txt": "___validation_token_received_from_the_ca___"}' \ 219 | https://auth.example.org/update 220 | ``` 221 | 222 | Note: The `txt` field must be exactly 43 characters long, otherwise acme-dns will reject it 223 | 224 | 4) Perform a DNS lookup to the test subdomain to confirm the updated TXT record is being served: 225 | ``` 226 | $ dig -t txt @auth.example.org d420c923-bbd7-4056-ab64-c3ca54c9b3cf.auth.example.org 227 | ``` 228 | 229 | ## Configuration 230 | 231 | ```bash 232 | [general] 233 | # DNS interface. Note that systemd-resolved may reserve port 53 on 127.0.0.53 234 | # In this case acme-dns will error out and you will need to define the listening interface 235 | # for example: listen = "127.0.0.1:53" 236 | listen = "127.0.0.1:53" 237 | # protocol, "both", "both4", "both6", "udp", "udp4", "udp6" or "tcp", "tcp4", "tcp6" 238 | protocol = "both" 239 | # domain name to serve the requests off of 240 | domain = "auth.example.org" 241 | # zone name server 242 | nsname = "auth.example.org" 243 | # admin email address, where @ is substituted with . 244 | nsadmin = "admin.example.org" 245 | # predefined records served in addition to the TXT 246 | records = [ 247 | # domain pointing to the public IP of your acme-dns server 248 | "auth.example.org. A 198.51.100.1", 249 | # specify that auth.example.org will resolve any *.auth.example.org records 250 | "auth.example.org. NS auth.example.org.", 251 | ] 252 | # debug messages from CORS etc 253 | debug = false 254 | 255 | [database] 256 | # Database engine to use, sqlite3 or postgres 257 | engine = "sqlite3" 258 | # Connection string, filename for sqlite3 and postgres://$username:$password@$host/$db_name for postgres 259 | # Please note that the default Docker image uses path /var/lib/acme-dns/acme-dns.db for sqlite3 260 | connection = "/var/lib/acme-dns/acme-dns.db" 261 | # connection = "postgres://user:password@localhost/acmedns_db" 262 | 263 | [api] 264 | # listen ip eg. 127.0.0.1 265 | ip = "0.0.0.0" 266 | # disable registration endpoint 267 | disable_registration = false 268 | # listen port, eg. 443 for default HTTPS 269 | port = "443" 270 | # possible values: "letsencrypt", "letsencryptstaging", "cert", "none" 271 | tls = "letsencryptstaging" 272 | # only used if tls = "cert" 273 | tls_cert_privkey = "/etc/tls/example.org/privkey.pem" 274 | tls_cert_fullchain = "/etc/tls/example.org/fullchain.pem" 275 | # only used if tls = "letsencrypt" 276 | acme_cache_dir = "api-certs" 277 | # optional e-mail address to which Let's Encrypt will send expiration notices for the API's cert 278 | notification_email = "" 279 | # CORS AllowOrigins, wildcards can be used 280 | corsorigins = [ 281 | "*" 282 | ] 283 | # use HTTP header to get the client ip 284 | use_header = false 285 | # header name to pull the ip address / list of ip addresses from 286 | header_name = "X-Forwarded-For" 287 | 288 | [logconfig] 289 | # logging level: "error", "warning", "info" or "debug" 290 | loglevel = "debug" 291 | # possible values: stdout, TODO file & integrations 292 | logtype = "stdout" 293 | # file path for logfile TODO 294 | # logfile = "./acme-dns.log" 295 | # format, either "json" or "text" 296 | logformat = "text" 297 | ``` 298 | 299 | ## HTTPS API 300 | 301 | The RESTful acme-dns API can be exposed over HTTPS in two ways: 302 | 303 | 1. Using `tls = "letsencrypt"` and letting acme-dns issue its own certificate 304 | automatically with Let's Encrypt. 305 | 1. Using `tls = "cert"` and providing your own HTTPS certificate chain and 306 | private key with `tls_cert_fullchain` and `tls_cert_privkey`. 307 | 308 | Where possible the first option is recommended. This is the easiest and safest 309 | way to have acme-dns expose its API over HTTPS. 310 | 311 | **Warning**: If you choose to use `tls = "cert"` you must take care that the 312 | certificate *does not expire*! If it does and the ACME client you use to issue the 313 | certificate depends on the ACME DNS API to update TXT records you will be stuck 314 | in a position where the API certificate has expired but it can't be renewed 315 | because the ACME client will refuse to connect to the ACME DNS API it needs to 316 | use for the renewal. 317 | 318 | ## Clients 319 | 320 | - acme.sh: [https://github.com/Neilpang/acme.sh](https://github.com/Neilpang/acme.sh) 321 | - Certify The Web: [https://github.com/webprofusion/certify](https://github.com/webprofusion/certify) 322 | - cert-manager: [https://github.com/jetstack/cert-manager](https://github.com/jetstack/cert-manager) 323 | - Lego: [https://github.com/xenolf/lego](https://github.com/xenolf/lego) 324 | - Posh-ACME: [https://github.com/rmbolger/Posh-ACME](https://github.com/rmbolger/Posh-ACME) 325 | - Sewer: [https://github.com/komuw/sewer](https://github.com/komuw/sewer) 326 | - Traefik: [https://github.com/containous/traefik](https://github.com/containous/traefik) 327 | - Windows ACME Simple (WACS): [https://www.win-acme.com](https://www.win-acme.com) 328 | 329 | ### Authentication hooks 330 | 331 | - acme-dns-client with Certbot authentication hook: [https://github.com/acme-dns/acme-dns-client](https://github.com/acme-dns/acme-dns-client) 332 | - Certbot authentication hook in Python: [https://github.com/joohoi/acme-dns-certbot-joohoi](https://github.com/joohoi/acme-dns-certbot-joohoi) 333 | - Certbot authentication hook in Go: [https://github.com/koesie10/acme-dns-certbot-hook](https://github.com/koesie10/acme-dns-certbot-hook) 334 | 335 | ### Libraries 336 | 337 | - Generic client library in Python ([PyPI](https://pypi.python.org/pypi/pyacmedns/)): [https://github.com/joohoi/pyacmedns](https://github.com/joohoi/pyacmedns) 338 | - Generic client library in Go: [https://github.com/cpu/goacmedns](https://github.com/cpu/goacmedns) 339 | 340 | 341 | ## Changelog 342 | 343 | - v0.8 344 | - NOTE: configuration option: "api_domain" deprecated! 345 | - New 346 | - 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. 347 | - 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. 348 | - Changed 349 | - Fixed: EDNS0 support 350 | - Migrated from autocert to [certmagic](https://github.com/mholt/certmagic) for HTTP API certificate handling 351 | - v0.7.2 352 | - Changed 353 | - Fixed: Regression error of not being able to answer to incoming random-case requests. 354 | - Fixed: SOA record added to a correct header field in NXDOMAIN responses. 355 | - v0.7.1 356 | - Changed 357 | - Fixed: SOA record correctly added to the TCP DNS server when using both, UDP and TCP servers. 358 | - v0.7 359 | - New 360 | - Added an endpoint to perform health checks 361 | - Changed 362 | - A new protocol selection for DNS server "both", that binds both - UDP and TCP ports. 363 | - Refactored DNS server internals. 364 | - Handle some aspects of DNS spec better. 365 | - v0.6 366 | - New 367 | - Command line flag `-c` to specify location of config file. 368 | - Proper refusal of dynamic update requests. 369 | - Release signing 370 | - Changed 371 | - Better error messages for goroutines 372 | - v0.5 373 | - New 374 | - Configurable certificate cache directory 375 | - Changed 376 | - Process wide umask to ensure created files are only readable by the user running acme-dns 377 | - Replaced package that handles UUIDs because of a flaw in the original package 378 | - Updated dependencies 379 | - Better error messages 380 | - 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. 381 | - v0.3.2 Dockerfile was fixed for users using autocert feature 382 | - v0.3.1 Added goreleaser for distributing binary builds of the releases 383 | - v0.3 Changed autocert to use HTTP-01 challenges, as TLS-SNI is disabled by Let's Encrypt 384 | - v0.2 Now powered by httprouter, support wildcard certificates, Docker images 385 | - v0.1 Initial release 386 | 387 | ## TODO 388 | 389 | - Logging to a file 390 | - DNSSEC 391 | - Want to see something implemented, make a feature request! 392 | 393 | ## Contributing 394 | 395 | acme-dns is open for contributions. 396 | If you have an idea for improvement, please open an new issue or feel free to write a PR! 397 | 398 | ## License 399 | 400 | acme-dns is released under the [MIT License](http://www.opensource.org/licenses/MIT). 401 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # Vagrantfile for running integration tests with PostgreSQL 5 | 6 | VAGRANTFILE_API_VERSION = "2" 7 | 8 | $ubuntu_setup_script = <> /home/vagrant/.profile 15 | echo "export GOPATH=/home/vagrant" >> /home/vagrant/.profile 16 | mkdir -p /home/vagrant/src/acme-dns 17 | chown -R vagrant /home/vagrant/src 18 | cp /vagrant/test/run_integration.sh /home/vagrant 19 | bash /vagrant/test/pgsql.sh 20 | echo "\n-------------------------------------------------------------" 21 | echo "To run integration tests run, /home/vagrant/run_integration.sh" 22 | SETUP_SCRIPT 23 | 24 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 25 | 26 | config.vm.define "ad-ubuntu-trusty", primary: true do |ad_ubuntu_trusty| 27 | ad_ubuntu_trusty.vm.box = "ubuntu/trusty64" 28 | ad_ubuntu_trusty.vm.provision "shell", inline: $ubuntu_setup_script 29 | ad_ubuntu_trusty.vm.network "forwarded_port", guest: 8080, host: 8008 30 | ad_ubuntu_trusty 31 | ad_ubuntu_trusty.vm.provider "virtualbox" do |v| 32 | v.memory = 2048 33 | end 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /acme-dns.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Limited DNS server with RESTful HTTP API to handle ACME DNS challenges easily and securely 3 | After=network.target 4 | 5 | [Service] 6 | User=acme-dns 7 | Group=acme-dns 8 | AmbientCapabilities=CAP_NET_BIND_SERVICE 9 | WorkingDirectory=~ 10 | ExecStart=/usr/local/bin/acme-dns 11 | Restart=on-failure 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /acmetxt.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "net" 6 | 7 | "github.com/google/uuid" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | // ACMETxt is the default structure for the user controlled record 12 | type ACMETxt struct { 13 | Username uuid.UUID 14 | Password string 15 | ACMETxtPost 16 | AllowFrom cidrslice 17 | } 18 | 19 | // ACMETxtPost holds the DNS part of the ACMETxt struct 20 | type ACMETxtPost struct { 21 | Subdomain string `json:"subdomain"` 22 | Value string `json:"txt"` 23 | } 24 | 25 | // cidrslice is a list of allowed cidr ranges 26 | type cidrslice []string 27 | 28 | func (c *cidrslice) JSON() string { 29 | ret, _ := json.Marshal(c.ValidEntries()) 30 | return string(ret) 31 | } 32 | 33 | func (c *cidrslice) isValid() error { 34 | for _, v := range *c { 35 | _, _, err := net.ParseCIDR(sanitizeIPv6addr(v)) 36 | if err != nil { 37 | return err 38 | } 39 | } 40 | return nil 41 | } 42 | 43 | func (c *cidrslice) ValidEntries() []string { 44 | valid := []string{} 45 | for _, v := range *c { 46 | _, _, err := net.ParseCIDR(sanitizeIPv6addr(v)) 47 | if err == nil { 48 | valid = append(valid, sanitizeIPv6addr(v)) 49 | } 50 | } 51 | return valid 52 | } 53 | 54 | // Check if IP belongs to an allowed net 55 | func (a ACMETxt) allowedFrom(ip string) bool { 56 | remoteIP := net.ParseIP(ip) 57 | // Range not limited 58 | if len(a.AllowFrom.ValidEntries()) == 0 { 59 | return true 60 | } 61 | log.WithFields(log.Fields{"ip": remoteIP}).Debug("Checking if update is permitted from IP") 62 | for _, v := range a.AllowFrom.ValidEntries() { 63 | _, vnet, _ := net.ParseCIDR(v) 64 | if vnet.Contains(remoteIP) { 65 | return true 66 | } 67 | } 68 | return false 69 | } 70 | 71 | // Go through list (most likely from headers) to check for the IP. 72 | // Reason for this is that some setups use reverse proxy in front of acme-dns 73 | func (a ACMETxt) allowedFromList(ips []string) bool { 74 | if len(ips) == 0 { 75 | // If no IP provided, check if no whitelist present (everyone has access) 76 | return a.allowedFrom("") 77 | } 78 | for _, v := range ips { 79 | if a.allowedFrom(v) { 80 | return true 81 | } 82 | } 83 | return false 84 | } 85 | 86 | func newACMETxt() ACMETxt { 87 | var a = ACMETxt{} 88 | password := generatePassword(40) 89 | a.Username = uuid.New() 90 | a.Password = password 91 | a.Subdomain = uuid.New().String() 92 | return a 93 | } 94 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | 9 | "github.com/julienschmidt/httprouter" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | // RegResponse is a struct for registration response JSON 14 | type RegResponse struct { 15 | Username string `json:"username"` 16 | Password string `json:"password"` 17 | Fulldomain string `json:"fulldomain"` 18 | Subdomain string `json:"subdomain"` 19 | Allowfrom []string `json:"allowfrom"` 20 | } 21 | 22 | func webRegisterPost(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { 23 | var regStatus int 24 | var reg []byte 25 | var err error 26 | aTXT := ACMETxt{} 27 | bdata, _ := io.ReadAll(r.Body) 28 | if len(bdata) > 0 { 29 | err = json.Unmarshal(bdata, &aTXT) 30 | if err != nil { 31 | regStatus = http.StatusBadRequest 32 | reg = jsonError("malformed_json_payload") 33 | w.Header().Set("Content-Type", "application/json") 34 | w.WriteHeader(regStatus) 35 | _, _ = w.Write(reg) 36 | return 37 | } 38 | } 39 | 40 | // Fail with malformed CIDR mask in allowfrom 41 | err = aTXT.AllowFrom.isValid() 42 | if err != nil { 43 | regStatus = http.StatusBadRequest 44 | reg = jsonError("invalid_allowfrom_cidr") 45 | w.Header().Set("Content-Type", "application/json") 46 | w.WriteHeader(regStatus) 47 | _, _ = w.Write(reg) 48 | return 49 | } 50 | 51 | // Create new user 52 | nu, err := DB.Register(aTXT.AllowFrom) 53 | if err != nil { 54 | errstr := fmt.Sprintf("%v", err) 55 | reg = jsonError(errstr) 56 | regStatus = http.StatusInternalServerError 57 | log.WithFields(log.Fields{"error": err.Error()}).Debug("Error in registration") 58 | } else { 59 | log.WithFields(log.Fields{"user": nu.Username.String()}).Debug("Created new user") 60 | regStruct := RegResponse{nu.Username.String(), nu.Password, nu.Subdomain + "." + Config.General.Domain, nu.Subdomain, nu.AllowFrom.ValidEntries()} 61 | regStatus = http.StatusCreated 62 | reg, err = json.Marshal(regStruct) 63 | if err != nil { 64 | regStatus = http.StatusInternalServerError 65 | reg = jsonError("json_error") 66 | log.WithFields(log.Fields{"error": "json"}).Debug("Could not marshal JSON") 67 | } 68 | } 69 | w.Header().Set("Content-Type", "application/json") 70 | w.WriteHeader(regStatus) 71 | _, _ = w.Write(reg) 72 | } 73 | 74 | func webUpdatePost(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { 75 | var updStatus int 76 | var upd []byte 77 | // Get user 78 | a, ok := r.Context().Value(ACMETxtKey).(ACMETxt) 79 | if !ok { 80 | log.WithFields(log.Fields{"error": "context"}).Error("Context error") 81 | } 82 | // NOTE: An invalid subdomain should not happen - the auth handler should 83 | // reject POSTs with an invalid subdomain before this handler. Reject any 84 | // invalid subdomains anyway as a matter of caution. 85 | if !validSubdomain(a.Subdomain) { 86 | log.WithFields(log.Fields{"error": "subdomain", "subdomain": a.Subdomain, "txt": a.Value}).Debug("Bad update data") 87 | updStatus = http.StatusBadRequest 88 | upd = jsonError("bad_subdomain") 89 | } else if !validTXT(a.Value) { 90 | log.WithFields(log.Fields{"error": "txt", "subdomain": a.Subdomain, "txt": a.Value}).Debug("Bad update data") 91 | updStatus = http.StatusBadRequest 92 | upd = jsonError("bad_txt") 93 | } else if validSubdomain(a.Subdomain) && validTXT(a.Value) { 94 | err := DB.Update(a.ACMETxtPost) 95 | if err != nil { 96 | log.WithFields(log.Fields{"error": err.Error()}).Debug("Error while trying to update record") 97 | updStatus = http.StatusInternalServerError 98 | upd = jsonError("db_error") 99 | } else { 100 | log.WithFields(log.Fields{"subdomain": a.Subdomain, "txt": a.Value}).Debug("TXT updated") 101 | updStatus = http.StatusOK 102 | upd = []byte("{\"txt\": \"" + a.Value + "\"}") 103 | } 104 | } 105 | w.Header().Set("Content-Type", "application/json") 106 | w.WriteHeader(updStatus) 107 | _, _ = w.Write(upd) 108 | } 109 | 110 | // Endpoint used to check the readiness and/or liveness (health) of the server. 111 | func healthCheck(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { 112 | w.WriteHeader(http.StatusOK) 113 | } 114 | -------------------------------------------------------------------------------- /api_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | 11 | "github.com/DATA-DOG/go-sqlmock" 12 | "github.com/gavv/httpexpect" 13 | "github.com/google/uuid" 14 | "github.com/julienschmidt/httprouter" 15 | "github.com/rs/cors" 16 | ) 17 | 18 | // noAuth function to write ACMETxt model to context while not preforming any validation 19 | func noAuth(update httprouter.Handle) httprouter.Handle { 20 | return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { 21 | postData := ACMETxt{} 22 | uname := r.Header.Get("X-Api-User") 23 | passwd := r.Header.Get("X-Api-Key") 24 | 25 | dec := json.NewDecoder(r.Body) 26 | _ = dec.Decode(&postData) 27 | // Set user info to the decoded ACMETxt object 28 | postData.Username, _ = uuid.Parse(uname) 29 | postData.Password = passwd 30 | // Set the ACMETxt struct to context to pull in from update function 31 | ctx := r.Context() 32 | ctx = context.WithValue(ctx, ACMETxtKey, postData) 33 | r = r.WithContext(ctx) 34 | update(w, r, p) 35 | } 36 | } 37 | 38 | func getExpect(t *testing.T, server *httptest.Server) *httpexpect.Expect { 39 | return httpexpect.WithConfig(httpexpect.Config{ 40 | BaseURL: server.URL, 41 | Reporter: httpexpect.NewAssertReporter(t), 42 | Printers: []httpexpect.Printer{ 43 | httpexpect.NewCurlPrinter(t), 44 | httpexpect.NewDebugPrinter(t, true), 45 | }, 46 | }) 47 | } 48 | 49 | func setupRouter(debug bool, noauth bool) http.Handler { 50 | api := httprouter.New() 51 | var dbcfg = dbsettings{ 52 | Engine: "sqlite3", 53 | Connection: ":memory:"} 54 | var httpapicfg = httpapi{ 55 | Domain: "", 56 | Port: "8080", 57 | TLS: "none", 58 | CorsOrigins: []string{"*"}, 59 | UseHeader: true, 60 | HeaderName: "X-Forwarded-For", 61 | } 62 | var dnscfg = DNSConfig{ 63 | API: httpapicfg, 64 | Database: dbcfg, 65 | } 66 | Config = dnscfg 67 | c := cors.New(cors.Options{ 68 | AllowedOrigins: Config.API.CorsOrigins, 69 | AllowedMethods: []string{"GET", "POST"}, 70 | OptionsPassthrough: false, 71 | Debug: Config.General.Debug, 72 | }) 73 | api.POST("/register", webRegisterPost) 74 | api.GET("/health", healthCheck) 75 | if noauth { 76 | api.POST("/update", noAuth(webUpdatePost)) 77 | } else { 78 | api.POST("/update", Auth(webUpdatePost)) 79 | } 80 | return c.Handler(api) 81 | } 82 | 83 | func TestApiRegister(t *testing.T) { 84 | router := setupRouter(false, false) 85 | server := httptest.NewServer(router) 86 | defer server.Close() 87 | e := getExpect(t, server) 88 | e.POST("/register").Expect(). 89 | Status(http.StatusCreated). 90 | JSON().Object(). 91 | ContainsKey("fulldomain"). 92 | ContainsKey("subdomain"). 93 | ContainsKey("username"). 94 | ContainsKey("password"). 95 | NotContainsKey("error") 96 | 97 | allowfrom := map[string][]interface{}{ 98 | "allowfrom": []interface{}{"123.123.123.123/32", 99 | "2001:db8:a0b:12f0::1/32", 100 | "[::1]/64", 101 | }, 102 | } 103 | 104 | response := e.POST("/register"). 105 | WithJSON(allowfrom). 106 | Expect(). 107 | Status(http.StatusCreated). 108 | JSON().Object(). 109 | ContainsKey("fulldomain"). 110 | ContainsKey("subdomain"). 111 | ContainsKey("username"). 112 | ContainsKey("password"). 113 | ContainsKey("allowfrom"). 114 | NotContainsKey("error") 115 | 116 | response.Value("allowfrom").Array().Elements("123.123.123.123/32", "2001:db8:a0b:12f0::1/32", "::1/64") 117 | } 118 | 119 | func TestApiRegisterBadAllowFrom(t *testing.T) { 120 | router := setupRouter(false, false) 121 | server := httptest.NewServer(router) 122 | defer server.Close() 123 | e := getExpect(t, server) 124 | invalidVals := []string{ 125 | "invalid", 126 | "1.2.3.4/33", 127 | "1.2/24", 128 | "1.2.3.4", 129 | "12345:db8:a0b:12f0::1/32", 130 | "1234::123::123::1/32", 131 | } 132 | 133 | for _, v := range invalidVals { 134 | 135 | allowfrom := map[string][]interface{}{ 136 | "allowfrom": []interface{}{v}} 137 | 138 | response := e.POST("/register"). 139 | WithJSON(allowfrom). 140 | Expect(). 141 | Status(http.StatusBadRequest). 142 | JSON().Object(). 143 | ContainsKey("error") 144 | 145 | response.Value("error").Equal("invalid_allowfrom_cidr") 146 | } 147 | } 148 | 149 | func TestApiRegisterMalformedJSON(t *testing.T) { 150 | router := setupRouter(false, false) 151 | server := httptest.NewServer(router) 152 | defer server.Close() 153 | e := getExpect(t, server) 154 | 155 | malPayloads := []string{ 156 | "{\"allowfrom': '1.1.1.1/32'}", 157 | "\"allowfrom\": \"1.1.1.1/32\"", 158 | "{\"allowfrom\": \"[1.1.1.1/32]\"", 159 | "\"allowfrom\": \"1.1.1.1/32\"}", 160 | "{allowfrom: \"1.2.3.4\"}", 161 | "{allowfrom: [1.2.3.4]}", 162 | "whatever that's not a json payload", 163 | } 164 | for _, test := range malPayloads { 165 | e.POST("/register"). 166 | WithBytes([]byte(test)). 167 | Expect(). 168 | Status(http.StatusBadRequest). 169 | JSON().Object(). 170 | ContainsKey("error"). 171 | NotContainsKey("subdomain"). 172 | NotContainsKey("username") 173 | } 174 | } 175 | 176 | func TestApiRegisterWithMockDB(t *testing.T) { 177 | router := setupRouter(false, false) 178 | server := httptest.NewServer(router) 179 | defer server.Close() 180 | e := getExpect(t, server) 181 | oldDb := DB.GetBackend() 182 | db, mock, _ := sqlmock.New() 183 | DB.SetBackend(db) 184 | defer db.Close() 185 | mock.ExpectBegin() 186 | mock.ExpectPrepare("INSERT INTO records").WillReturnError(errors.New("error")) 187 | e.POST("/register").Expect(). 188 | Status(http.StatusInternalServerError). 189 | JSON().Object(). 190 | ContainsKey("error") 191 | DB.SetBackend(oldDb) 192 | } 193 | 194 | func TestApiUpdateWithInvalidSubdomain(t *testing.T) { 195 | validTxtData := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 196 | 197 | updateJSON := map[string]interface{}{ 198 | "subdomain": "", 199 | "txt": ""} 200 | 201 | router := setupRouter(false, false) 202 | server := httptest.NewServer(router) 203 | defer server.Close() 204 | e := getExpect(t, server) 205 | newUser, err := DB.Register(cidrslice{}) 206 | if err != nil { 207 | t.Errorf("Could not create new user, got error [%v]", err) 208 | } 209 | // Invalid subdomain data 210 | updateJSON["subdomain"] = "example.com" 211 | updateJSON["txt"] = validTxtData 212 | e.POST("/update"). 213 | WithJSON(updateJSON). 214 | WithHeader("X-Api-User", newUser.Username.String()). 215 | WithHeader("X-Api-Key", newUser.Password). 216 | Expect(). 217 | Status(http.StatusUnauthorized). 218 | JSON().Object(). 219 | ContainsKey("error"). 220 | NotContainsKey("txt"). 221 | ValueEqual("error", "forbidden") 222 | } 223 | 224 | func TestApiUpdateWithInvalidTxt(t *testing.T) { 225 | invalidTXTData := "idk m8 bbl lmao" 226 | 227 | updateJSON := map[string]interface{}{ 228 | "subdomain": "", 229 | "txt": ""} 230 | 231 | router := setupRouter(false, false) 232 | server := httptest.NewServer(router) 233 | defer server.Close() 234 | e := getExpect(t, server) 235 | newUser, err := DB.Register(cidrslice{}) 236 | if err != nil { 237 | t.Errorf("Could not create new user, got error [%v]", err) 238 | } 239 | updateJSON["subdomain"] = newUser.Subdomain 240 | // Invalid txt data 241 | updateJSON["txt"] = invalidTXTData 242 | e.POST("/update"). 243 | WithJSON(updateJSON). 244 | WithHeader("X-Api-User", newUser.Username.String()). 245 | WithHeader("X-Api-Key", newUser.Password). 246 | Expect(). 247 | Status(http.StatusBadRequest). 248 | JSON().Object(). 249 | ContainsKey("error"). 250 | NotContainsKey("txt"). 251 | ValueEqual("error", "bad_txt") 252 | } 253 | 254 | func TestApiUpdateWithoutCredentials(t *testing.T) { 255 | router := setupRouter(false, false) 256 | server := httptest.NewServer(router) 257 | defer server.Close() 258 | e := getExpect(t, server) 259 | e.POST("/update").Expect(). 260 | Status(http.StatusUnauthorized). 261 | JSON().Object(). 262 | ContainsKey("error"). 263 | NotContainsKey("txt") 264 | } 265 | 266 | func TestApiUpdateWithCredentials(t *testing.T) { 267 | validTxtData := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 268 | 269 | updateJSON := map[string]interface{}{ 270 | "subdomain": "", 271 | "txt": ""} 272 | 273 | router := setupRouter(false, false) 274 | server := httptest.NewServer(router) 275 | defer server.Close() 276 | e := getExpect(t, server) 277 | newUser, err := DB.Register(cidrslice{}) 278 | if err != nil { 279 | t.Errorf("Could not create new user, got error [%v]", err) 280 | } 281 | // Valid data 282 | updateJSON["subdomain"] = newUser.Subdomain 283 | updateJSON["txt"] = validTxtData 284 | e.POST("/update"). 285 | WithJSON(updateJSON). 286 | WithHeader("X-Api-User", newUser.Username.String()). 287 | WithHeader("X-Api-Key", newUser.Password). 288 | Expect(). 289 | Status(http.StatusOK). 290 | JSON().Object(). 291 | ContainsKey("txt"). 292 | NotContainsKey("error"). 293 | ValueEqual("txt", validTxtData) 294 | } 295 | 296 | func TestApiUpdateWithCredentialsMockDB(t *testing.T) { 297 | validTxtData := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 298 | updateJSON := map[string]interface{}{ 299 | "subdomain": "", 300 | "txt": ""} 301 | 302 | // Valid data 303 | updateJSON["subdomain"] = "a097455b-52cc-4569-90c8-7a4b97c6eba8" 304 | updateJSON["txt"] = validTxtData 305 | 306 | router := setupRouter(false, true) 307 | server := httptest.NewServer(router) 308 | defer server.Close() 309 | e := getExpect(t, server) 310 | oldDb := DB.GetBackend() 311 | db, mock, _ := sqlmock.New() 312 | DB.SetBackend(db) 313 | defer db.Close() 314 | mock.ExpectBegin() 315 | mock.ExpectPrepare("UPDATE records").WillReturnError(errors.New("error")) 316 | e.POST("/update"). 317 | WithJSON(updateJSON). 318 | Expect(). 319 | Status(http.StatusInternalServerError). 320 | JSON().Object(). 321 | ContainsKey("error") 322 | DB.SetBackend(oldDb) 323 | } 324 | 325 | func TestApiManyUpdateWithCredentials(t *testing.T) { 326 | validTxtData := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 327 | 328 | router := setupRouter(true, false) 329 | server := httptest.NewServer(router) 330 | defer server.Close() 331 | e := getExpect(t, server) 332 | // User without defined CIDR masks 333 | newUser, err := DB.Register(cidrslice{}) 334 | if err != nil { 335 | t.Errorf("Could not create new user, got error [%v]", err) 336 | } 337 | 338 | // User with defined allow from - CIDR masks, all invalid 339 | // (httpexpect doesn't provide a way to mock remote ip) 340 | newUserWithCIDR, err := DB.Register(cidrslice{"192.168.1.1/32", "invalid"}) 341 | if err != nil { 342 | t.Errorf("Could not create new user with CIDR, got error [%v]", err) 343 | } 344 | 345 | // Another user with valid CIDR mask to match the httpexpect default 346 | newUserWithValidCIDR, err := DB.Register(cidrslice{"10.1.2.3/32", "invalid"}) 347 | if err != nil { 348 | t.Errorf("Could not create new user with a valid CIDR, got error [%v]", err) 349 | } 350 | 351 | for _, test := range []struct { 352 | user string 353 | pass string 354 | subdomain string 355 | txt interface{} 356 | status int 357 | }{ 358 | {"non-uuid-user", "tooshortpass", "non-uuid-subdomain", validTxtData, 401}, 359 | {"a097455b-52cc-4569-90c8-7a4b97c6eba8", "tooshortpass", "bb97455b-52cc-4569-90c8-7a4b97c6eba8", validTxtData, 401}, 360 | {"a097455b-52cc-4569-90c8-7a4b97c6eba8", "LongEnoughPassButNoUserExists___________", "bb97455b-52cc-4569-90c8-7a4b97c6eba8", validTxtData, 401}, 361 | {newUser.Username.String(), newUser.Password, "a097455b-52cc-4569-90c8-7a4b97c6eba8", validTxtData, 401}, 362 | {newUser.Username.String(), newUser.Password, newUser.Subdomain, "tooshortfortxt", 400}, 363 | {newUser.Username.String(), newUser.Password, newUser.Subdomain, 1234567890, 400}, 364 | {newUser.Username.String(), newUser.Password, newUser.Subdomain, validTxtData, 200}, 365 | {newUserWithCIDR.Username.String(), newUserWithCIDR.Password, newUserWithCIDR.Subdomain, validTxtData, 401}, 366 | {newUserWithValidCIDR.Username.String(), newUserWithValidCIDR.Password, newUserWithValidCIDR.Subdomain, validTxtData, 200}, 367 | {newUser.Username.String(), "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", newUser.Subdomain, validTxtData, 401}, 368 | } { 369 | updateJSON := map[string]interface{}{ 370 | "subdomain": test.subdomain, 371 | "txt": test.txt} 372 | e.POST("/update"). 373 | WithJSON(updateJSON). 374 | WithHeader("X-Api-User", test.user). 375 | WithHeader("X-Api-Key", test.pass). 376 | WithHeader("X-Forwarded-For", "10.1.2.3"). 377 | Expect(). 378 | Status(test.status) 379 | } 380 | } 381 | 382 | func TestApiManyUpdateWithIpCheckHeaders(t *testing.T) { 383 | 384 | router := setupRouter(false, false) 385 | server := httptest.NewServer(router) 386 | defer server.Close() 387 | e := getExpect(t, server) 388 | // Use header checks from default header (X-Forwarded-For) 389 | Config.API.UseHeader = true 390 | // User without defined CIDR masks 391 | newUser, err := DB.Register(cidrslice{}) 392 | if err != nil { 393 | t.Errorf("Could not create new user, got error [%v]", err) 394 | } 395 | 396 | newUserWithCIDR, err := DB.Register(cidrslice{"192.168.1.2/32", "invalid"}) 397 | if err != nil { 398 | t.Errorf("Could not create new user with CIDR, got error [%v]", err) 399 | } 400 | 401 | newUserWithIP6CIDR, err := DB.Register(cidrslice{"2002:c0a8::0/32"}) 402 | if err != nil { 403 | t.Errorf("Could not create a new user with IP6 CIDR, got error [%v]", err) 404 | } 405 | 406 | for _, test := range []struct { 407 | user ACMETxt 408 | headerValue string 409 | status int 410 | }{ 411 | {newUser, "whatever goes", 200}, 412 | {newUser, "10.0.0.1, 1.2.3.4 ,3.4.5.6", 200}, 413 | {newUserWithCIDR, "127.0.0.1", 401}, 414 | {newUserWithCIDR, "10.0.0.1, 10.0.0.2, 192.168.1.3", 401}, 415 | {newUserWithCIDR, "10.1.1.1 ,192.168.1.2, 8.8.8.8", 200}, 416 | {newUserWithIP6CIDR, "2002:c0a8:b4dc:0d3::0", 200}, 417 | {newUserWithIP6CIDR, "2002:c0a7:0ff::0", 401}, 418 | {newUserWithIP6CIDR, "2002:c0a8:d3ad:b33f:c0ff:33b4:dc0d:3b4d", 200}, 419 | } { 420 | updateJSON := map[string]interface{}{ 421 | "subdomain": test.user.Subdomain, 422 | "txt": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"} 423 | e.POST("/update"). 424 | WithJSON(updateJSON). 425 | WithHeader("X-Api-User", test.user.Username.String()). 426 | WithHeader("X-Api-Key", test.user.Password). 427 | WithHeader("X-Forwarded-For", test.headerValue). 428 | Expect(). 429 | Status(test.status) 430 | } 431 | Config.API.UseHeader = false 432 | } 433 | 434 | func TestApiHealthCheck(t *testing.T) { 435 | router := setupRouter(false, false) 436 | server := httptest.NewServer(router) 437 | defer server.Close() 438 | e := getExpect(t, server) 439 | e.GET("/health").Expect().Status(http.StatusOK) 440 | } 441 | -------------------------------------------------------------------------------- /auth.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net" 8 | "net/http" 9 | 10 | "github.com/julienschmidt/httprouter" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | type key int 15 | 16 | // ACMETxtKey is a context key for ACMETxt struct 17 | const ACMETxtKey key = 0 18 | 19 | // Auth middleware for update request 20 | func Auth(update httprouter.Handle) httprouter.Handle { 21 | return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { 22 | postData := ACMETxt{} 23 | userOK := false 24 | user, err := getUserFromRequest(r) 25 | if err == nil { 26 | if updateAllowedFromIP(r, user) { 27 | dec := json.NewDecoder(r.Body) 28 | err = dec.Decode(&postData) 29 | if err != nil { 30 | log.WithFields(log.Fields{"error": "json_error", "string": err.Error()}).Error("Decode error") 31 | } 32 | if user.Subdomain == postData.Subdomain { 33 | userOK = true 34 | } else { 35 | log.WithFields(log.Fields{"error": "subdomain_mismatch", "name": postData.Subdomain, "expected": user.Subdomain}).Error("Subdomain mismatch") 36 | } 37 | } else { 38 | log.WithFields(log.Fields{"error": "ip_unauthorized"}).Error("Update not allowed from IP") 39 | } 40 | } else { 41 | log.WithFields(log.Fields{"error": err.Error()}).Error("Error while trying to get user") 42 | } 43 | if userOK { 44 | // Set user info to the decoded ACMETxt object 45 | postData.Username = user.Username 46 | postData.Password = user.Password 47 | // Set the ACMETxt struct to context to pull in from update function 48 | ctx := context.WithValue(r.Context(), ACMETxtKey, postData) 49 | update(w, r.WithContext(ctx), p) 50 | } else { 51 | w.Header().Set("Content-Type", "application/json") 52 | w.WriteHeader(http.StatusUnauthorized) 53 | _, _ = w.Write(jsonError("forbidden")) 54 | } 55 | } 56 | } 57 | 58 | func getUserFromRequest(r *http.Request) (ACMETxt, error) { 59 | uname := r.Header.Get("X-Api-User") 60 | passwd := r.Header.Get("X-Api-Key") 61 | username, err := getValidUsername(uname) 62 | if err != nil { 63 | return ACMETxt{}, fmt.Errorf("Invalid username: %s: %s", uname, err.Error()) 64 | } 65 | if validKey(passwd) { 66 | dbuser, err := DB.GetByUsername(username) 67 | if err != nil { 68 | log.WithFields(log.Fields{"error": err.Error()}).Error("Error while trying to get user") 69 | // To protect against timed side channel (never gonna give you up) 70 | correctPassword(passwd, "$2a$10$8JEFVNYYhLoBysjAxe2yBuXrkDojBQBkVpXEQgyQyjn43SvJ4vL36") 71 | 72 | return ACMETxt{}, fmt.Errorf("Invalid username: %s", uname) 73 | } 74 | if correctPassword(passwd, dbuser.Password) { 75 | return dbuser, nil 76 | } 77 | return ACMETxt{}, fmt.Errorf("Invalid password for user %s", uname) 78 | } 79 | return ACMETxt{}, fmt.Errorf("Invalid key for user %s", uname) 80 | } 81 | 82 | func updateAllowedFromIP(r *http.Request, user ACMETxt) bool { 83 | if Config.API.UseHeader { 84 | ips := getIPListFromHeader(r.Header.Get(Config.API.HeaderName)) 85 | return user.allowedFromList(ips) 86 | } 87 | host, _, err := net.SplitHostPort(r.RemoteAddr) 88 | if err != nil { 89 | log.WithFields(log.Fields{"error": err.Error(), "remoteaddr": r.RemoteAddr}).Error("Error while parsing remote address") 90 | host = "" 91 | } 92 | return user.allowedFrom(host) 93 | } 94 | -------------------------------------------------------------------------------- /auth_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | ) 7 | 8 | func TestUpdateAllowedFromIP(t *testing.T) { 9 | Config.API.UseHeader = false 10 | userWithAllow := newACMETxt() 11 | userWithAllow.AllowFrom = cidrslice{"192.168.1.2/32", "[::1]/128"} 12 | userWithoutAllow := newACMETxt() 13 | 14 | for i, test := range []struct { 15 | remoteaddr string 16 | expected bool 17 | }{ 18 | {"192.168.1.2:1234", true}, 19 | {"192.168.1.1:1234", false}, 20 | {"invalid", false}, 21 | {"[::1]:4567", true}, 22 | } { 23 | newreq, _ := http.NewRequest("GET", "/whatever", nil) 24 | newreq.RemoteAddr = test.remoteaddr 25 | ret := updateAllowedFromIP(newreq, userWithAllow) 26 | if test.expected != ret { 27 | t.Errorf("Test %d: Unexpected result for user with allowForm set", i) 28 | } 29 | 30 | if !updateAllowedFromIP(newreq, userWithoutAllow) { 31 | t.Errorf("Test %d: Unexpected result for user without allowForm set", i) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /challengeprovider.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/mholt/acmez/v2/acme" 7 | ) 8 | 9 | // ChallengeProvider implements go-acme/lego Provider interface which is used for ACME DNS challenge handling 10 | type ChallengeProvider struct { 11 | servers []*DNSServer 12 | } 13 | 14 | // NewChallengeProvider creates a new instance of ChallengeProvider 15 | func NewChallengeProvider(servers []*DNSServer) ChallengeProvider { 16 | return ChallengeProvider{servers: servers} 17 | } 18 | 19 | // Present is used for making the ACME DNS challenge token available for DNS 20 | func (c *ChallengeProvider) Present(ctx context.Context, challenge acme.Challenge) error { 21 | for _, s := range c.servers { 22 | s.PersonalKeyAuth = challenge.DNS01KeyAuthorization() 23 | } 24 | return nil 25 | } 26 | 27 | // CleanUp is called after the run to remove the ACME DNS challenge tokens from DNS records 28 | func (c *ChallengeProvider) CleanUp(ctx context.Context, _ acme.Challenge) error { 29 | for _, s := range c.servers { 30 | s.PersonalKeyAuth = "" 31 | } 32 | return nil 33 | } 34 | 35 | // Wait is a dummy function as we are just going to be ready to answer the challenge from the get-go 36 | func (c *ChallengeProvider) Wait(_ context.Context, _ acme.Challenge) error { 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /config.cfg: -------------------------------------------------------------------------------- 1 | [general] 2 | # DNS interface. Note that systemd-resolved may reserve port 53 on 127.0.0.53 3 | # In this case acme-dns will error out and you will need to define the listening interface 4 | # for example: listen = "127.0.0.1:53" 5 | listen = "127.0.0.1:53" 6 | # protocol, "both", "both4", "both6", "udp", "udp4", "udp6" or "tcp", "tcp4", "tcp6" 7 | protocol = "both" 8 | # domain name to serve the requests off of 9 | domain = "auth.example.org" 10 | # zone name server 11 | nsname = "auth.example.org" 12 | # admin email address, where @ is substituted with . 13 | nsadmin = "admin.example.org" 14 | # predefined records served in addition to the TXT 15 | records = [ 16 | # domain pointing to the public IP of your acme-dns server 17 | "auth.example.org. A 198.51.100.1", 18 | # specify that auth.example.org will resolve any *.auth.example.org records 19 | "auth.example.org. NS auth.example.org.", 20 | ] 21 | # debug messages from CORS etc 22 | debug = false 23 | 24 | [database] 25 | # Database engine to use, sqlite3 or postgres 26 | engine = "sqlite3" 27 | # Connection string, filename for sqlite3 and postgres://$username:$password@$host/$db_name for postgres 28 | # Please note that the default Docker image uses path /var/lib/acme-dns/acme-dns.db for sqlite3 29 | connection = "/var/lib/acme-dns/acme-dns.db" 30 | # connection = "postgres://user:password@localhost/acmedns_db" 31 | 32 | [api] 33 | # listen ip eg. 127.0.0.1 34 | ip = "0.0.0.0" 35 | # disable registration endpoint 36 | disable_registration = false 37 | # listen port, eg. 443 for default HTTPS 38 | port = "443" 39 | # possible values: "letsencrypt", "letsencryptstaging", "cert", "none" 40 | tls = "letsencryptstaging" 41 | # only used if tls = "cert" 42 | tls_cert_privkey = "/etc/tls/example.org/privkey.pem" 43 | tls_cert_fullchain = "/etc/tls/example.org/fullchain.pem" 44 | # only used if tls = "letsencrypt" 45 | acme_cache_dir = "api-certs" 46 | # optional e-mail address to which Let's Encrypt will send expiration notices for the API's cert 47 | notification_email = "" 48 | # CORS AllowOrigins, wildcards can be used 49 | corsorigins = [ 50 | "*" 51 | ] 52 | # use HTTP header to get the client ip 53 | use_header = false 54 | # header name to pull the ip address / list of ip addresses from 55 | header_name = "X-Forwarded-For" 56 | 57 | [logconfig] 58 | # logging level: "error", "warning", "info" or "debug" 59 | loglevel = "debug" 60 | # possible values: stdout, TODO file & integrations 61 | logtype = "stdout" 62 | # file path for logfile TODO 63 | # logfile = "./acme-dns.log" 64 | # format, either "json" or "text" 65 | logformat = "text" 66 | -------------------------------------------------------------------------------- /db.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "regexp" 9 | "strconv" 10 | "time" 11 | 12 | "github.com/google/uuid" 13 | _ "github.com/lib/pq" 14 | _ "github.com/mattn/go-sqlite3" 15 | log "github.com/sirupsen/logrus" 16 | "golang.org/x/crypto/bcrypt" 17 | ) 18 | 19 | // DBVersion shows the database version this code uses. This is used for update checks. 20 | var DBVersion = 1 21 | 22 | var acmeTable = ` 23 | CREATE TABLE IF NOT EXISTS acmedns( 24 | Name TEXT, 25 | Value TEXT 26 | );` 27 | 28 | var userTable = ` 29 | CREATE TABLE IF NOT EXISTS records( 30 | Username TEXT UNIQUE NOT NULL PRIMARY KEY, 31 | Password TEXT UNIQUE NOT NULL, 32 | Subdomain TEXT UNIQUE NOT NULL, 33 | AllowFrom TEXT 34 | );` 35 | 36 | var txtTable = ` 37 | CREATE TABLE IF NOT EXISTS txt( 38 | Subdomain TEXT NOT NULL, 39 | Value TEXT NOT NULL DEFAULT '', 40 | LastUpdate INT 41 | );` 42 | 43 | var txtTablePG = ` 44 | CREATE TABLE IF NOT EXISTS txt( 45 | rowid SERIAL, 46 | Subdomain TEXT NOT NULL, 47 | Value TEXT NOT NULL DEFAULT '', 48 | LastUpdate INT 49 | );` 50 | 51 | // getSQLiteStmt replaces all PostgreSQL prepared statement placeholders (eg. $1, $2) with SQLite variant "?" 52 | func getSQLiteStmt(s string) string { 53 | re, _ := regexp.Compile(`\$[0-9]`) 54 | return re.ReplaceAllString(s, "?") 55 | } 56 | 57 | func (d *acmedb) Init(engine string, connection string) error { 58 | d.Mutex.Lock() 59 | defer d.Mutex.Unlock() 60 | db, err := sql.Open(engine, connection) 61 | if err != nil { 62 | return err 63 | } 64 | d.DB = db 65 | // Check version first to try to catch old versions without version string 66 | var versionString string 67 | _ = d.DB.QueryRow("SELECT Value FROM acmedns WHERE Name='db_version'").Scan(&versionString) 68 | if versionString == "" { 69 | versionString = "0" 70 | } 71 | _, _ = d.DB.Exec(acmeTable) 72 | _, _ = d.DB.Exec(userTable) 73 | if Config.Database.Engine == "sqlite3" { 74 | _, _ = d.DB.Exec(txtTable) 75 | } else { 76 | _, _ = d.DB.Exec(txtTablePG) 77 | } 78 | // If everything is fine, handle db upgrade tasks 79 | if err == nil { 80 | err = d.checkDBUpgrades(versionString) 81 | } 82 | if err == nil { 83 | if versionString == "0" { 84 | // No errors so we should now be in version 1 85 | insversion := fmt.Sprintf("INSERT INTO acmedns (Name, Value) values('db_version', '%d')", DBVersion) 86 | _, err = db.Exec(insversion) 87 | } 88 | } 89 | return err 90 | } 91 | 92 | func (d *acmedb) checkDBUpgrades(versionString string) error { 93 | var err error 94 | version, err := strconv.Atoi(versionString) 95 | if err != nil { 96 | return err 97 | } 98 | if version != DBVersion { 99 | return d.handleDBUpgrades(version) 100 | } 101 | return nil 102 | 103 | } 104 | 105 | func (d *acmedb) handleDBUpgrades(version int) error { 106 | if version == 0 { 107 | return d.handleDBUpgradeTo1() 108 | } 109 | return nil 110 | } 111 | 112 | func (d *acmedb) handleDBUpgradeTo1() error { 113 | var err error 114 | var subdomains []string 115 | rows, err := d.DB.Query("SELECT Subdomain FROM records") 116 | if err != nil { 117 | log.WithFields(log.Fields{"error": err.Error()}).Error("Error in DB upgrade") 118 | return err 119 | } 120 | defer rows.Close() 121 | for rows.Next() { 122 | var subdomain string 123 | err = rows.Scan(&subdomain) 124 | if err != nil { 125 | log.WithFields(log.Fields{"error": err.Error()}).Error("Error in DB upgrade while reading values") 126 | return err 127 | } 128 | subdomains = append(subdomains, subdomain) 129 | } 130 | err = rows.Err() 131 | if err != nil { 132 | log.WithFields(log.Fields{"error": err.Error()}).Error("Error in DB upgrade while inserting values") 133 | return err 134 | } 135 | tx, err := d.DB.Begin() 136 | // Rollback if errored, commit if not 137 | defer func() { 138 | if err != nil { 139 | _ = tx.Rollback() 140 | return 141 | } 142 | _ = tx.Commit() 143 | }() 144 | _, _ = tx.Exec("DELETE FROM txt") 145 | for _, subdomain := range subdomains { 146 | if subdomain != "" { 147 | // Insert two rows for each subdomain to txt table 148 | err = d.NewTXTValuesInTransaction(tx, subdomain) 149 | if err != nil { 150 | log.WithFields(log.Fields{"error": err.Error()}).Error("Error in DB upgrade while inserting values") 151 | return err 152 | } 153 | } 154 | } 155 | // SQLite doesn't support dropping columns 156 | if Config.Database.Engine != "sqlite3" { 157 | _, _ = tx.Exec("ALTER TABLE records DROP COLUMN IF EXISTS Value") 158 | _, _ = tx.Exec("ALTER TABLE records DROP COLUMN IF EXISTS LastActive") 159 | } 160 | _, err = tx.Exec("UPDATE acmedns SET Value='1' WHERE Name='db_version'") 161 | return err 162 | } 163 | 164 | // Create two rows for subdomain to the txt table 165 | func (d *acmedb) NewTXTValuesInTransaction(tx *sql.Tx, subdomain string) error { 166 | var err error 167 | instr := fmt.Sprintf("INSERT INTO txt (Subdomain, LastUpdate) values('%s', 0)", subdomain) 168 | _, _ = tx.Exec(instr) 169 | _, _ = tx.Exec(instr) 170 | return err 171 | } 172 | 173 | func (d *acmedb) Register(afrom cidrslice) (ACMETxt, error) { 174 | d.Mutex.Lock() 175 | defer d.Mutex.Unlock() 176 | var err error 177 | tx, err := d.DB.Begin() 178 | // Rollback if errored, commit if not 179 | defer func() { 180 | if err != nil { 181 | _ = tx.Rollback() 182 | return 183 | } 184 | _ = tx.Commit() 185 | }() 186 | a := newACMETxt() 187 | a.AllowFrom = cidrslice(afrom.ValidEntries()) 188 | passwordHash, err := bcrypt.GenerateFromPassword([]byte(a.Password), 10) 189 | regSQL := ` 190 | INSERT INTO records( 191 | Username, 192 | Password, 193 | Subdomain, 194 | AllowFrom) 195 | values($1, $2, $3, $4)` 196 | if Config.Database.Engine == "sqlite3" { 197 | regSQL = getSQLiteStmt(regSQL) 198 | } 199 | sm, err := tx.Prepare(regSQL) 200 | if err != nil { 201 | log.WithFields(log.Fields{"error": err.Error()}).Error("Database error in prepare") 202 | return a, errors.New("SQL error") 203 | } 204 | defer sm.Close() 205 | _, err = sm.Exec(a.Username.String(), passwordHash, a.Subdomain, a.AllowFrom.JSON()) 206 | if err == nil { 207 | err = d.NewTXTValuesInTransaction(tx, a.Subdomain) 208 | } 209 | return a, err 210 | } 211 | 212 | func (d *acmedb) GetByUsername(u uuid.UUID) (ACMETxt, error) { 213 | d.Mutex.Lock() 214 | defer d.Mutex.Unlock() 215 | var results []ACMETxt 216 | getSQL := ` 217 | SELECT Username, Password, Subdomain, AllowFrom 218 | FROM records 219 | WHERE Username=$1 LIMIT 1 220 | ` 221 | if Config.Database.Engine == "sqlite3" { 222 | getSQL = getSQLiteStmt(getSQL) 223 | } 224 | 225 | sm, err := d.DB.Prepare(getSQL) 226 | if err != nil { 227 | return ACMETxt{}, err 228 | } 229 | defer sm.Close() 230 | rows, err := sm.Query(u.String()) 231 | if err != nil { 232 | return ACMETxt{}, err 233 | } 234 | defer rows.Close() 235 | 236 | // It will only be one row though 237 | for rows.Next() { 238 | txt, err := getModelFromRow(rows) 239 | if err != nil { 240 | return ACMETxt{}, err 241 | } 242 | results = append(results, txt) 243 | } 244 | if len(results) > 0 { 245 | return results[0], nil 246 | } 247 | return ACMETxt{}, errors.New("no user") 248 | } 249 | 250 | func (d *acmedb) GetTXTForDomain(domain string) ([]string, error) { 251 | d.Mutex.Lock() 252 | defer d.Mutex.Unlock() 253 | domain = sanitizeString(domain) 254 | var txts []string 255 | getSQL := ` 256 | SELECT Value FROM txt WHERE Subdomain=$1 LIMIT 2 257 | ` 258 | if Config.Database.Engine == "sqlite3" { 259 | getSQL = getSQLiteStmt(getSQL) 260 | } 261 | 262 | sm, err := d.DB.Prepare(getSQL) 263 | if err != nil { 264 | return txts, err 265 | } 266 | defer sm.Close() 267 | rows, err := sm.Query(domain) 268 | if err != nil { 269 | return txts, err 270 | } 271 | defer rows.Close() 272 | 273 | for rows.Next() { 274 | var rtxt string 275 | err = rows.Scan(&rtxt) 276 | if err != nil { 277 | return txts, err 278 | } 279 | txts = append(txts, rtxt) 280 | } 281 | return txts, nil 282 | } 283 | 284 | func (d *acmedb) Update(a ACMETxtPost) error { 285 | d.Mutex.Lock() 286 | defer d.Mutex.Unlock() 287 | var err error 288 | // Data in a is already sanitized 289 | timenow := time.Now().Unix() 290 | 291 | updSQL := ` 292 | UPDATE txt SET Value=$1, LastUpdate=$2 293 | WHERE rowid=( 294 | SELECT rowid FROM txt WHERE Subdomain=$3 ORDER BY LastUpdate LIMIT 1) 295 | ` 296 | if Config.Database.Engine == "sqlite3" { 297 | updSQL = getSQLiteStmt(updSQL) 298 | } 299 | 300 | sm, err := d.DB.Prepare(updSQL) 301 | if err != nil { 302 | return err 303 | } 304 | defer sm.Close() 305 | _, err = sm.Exec(a.Value, timenow, a.Subdomain) 306 | if err != nil { 307 | return err 308 | } 309 | return nil 310 | } 311 | 312 | func getModelFromRow(r *sql.Rows) (ACMETxt, error) { 313 | txt := ACMETxt{} 314 | afrom := "" 315 | err := r.Scan( 316 | &txt.Username, 317 | &txt.Password, 318 | &txt.Subdomain, 319 | &afrom) 320 | if err != nil { 321 | log.WithFields(log.Fields{"error": err.Error()}).Error("Row scan error") 322 | } 323 | 324 | cslice := cidrslice{} 325 | err = json.Unmarshal([]byte(afrom), &cslice) 326 | if err != nil { 327 | log.WithFields(log.Fields{"error": err.Error()}).Error("JSON unmarshall error") 328 | } 329 | txt.AllowFrom = cslice 330 | return txt, err 331 | } 332 | 333 | func (d *acmedb) Close() { 334 | d.DB.Close() 335 | } 336 | 337 | func (d *acmedb) GetBackend() *sql.DB { 338 | return d.DB 339 | } 340 | 341 | func (d *acmedb) SetBackend(backend *sql.DB) { 342 | d.DB = backend 343 | } 344 | -------------------------------------------------------------------------------- /db_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "database/sql/driver" 6 | "errors" 7 | "github.com/erikstmartin/go-testdb" 8 | "testing" 9 | ) 10 | 11 | type testResult struct { 12 | lastID int64 13 | affectedRows int64 14 | } 15 | 16 | func (r testResult) LastInsertId() (int64, error) { 17 | return r.lastID, nil 18 | } 19 | 20 | func (r testResult) RowsAffected() (int64, error) { 21 | return r.affectedRows, nil 22 | } 23 | 24 | func TestDBInit(t *testing.T) { 25 | fakeDB := new(acmedb) 26 | err := fakeDB.Init("notarealegine", "connectionstring") 27 | if err == nil { 28 | t.Errorf("Was expecting error, didn't get one.") 29 | } 30 | 31 | testdb.SetExecWithArgsFunc(func(query string, args []driver.Value) (result driver.Result, err error) { 32 | return testResult{1, 0}, errors.New("Prepared query error") 33 | }) 34 | defer testdb.Reset() 35 | 36 | errorDB := new(acmedb) 37 | err = errorDB.Init("testdb", "") 38 | if err == nil { 39 | t.Errorf("Was expecting DB initiation error but got none") 40 | } 41 | errorDB.Close() 42 | } 43 | 44 | func TestRegisterNoCIDR(t *testing.T) { 45 | // Register tests 46 | _, err := DB.Register(cidrslice{}) 47 | if err != nil { 48 | t.Errorf("Registration failed, got error [%v]", err) 49 | } 50 | } 51 | 52 | func TestRegisterMany(t *testing.T) { 53 | for i, test := range []struct { 54 | input cidrslice 55 | output cidrslice 56 | }{ 57 | {cidrslice{"127.0.0.1/8", "8.8.8.8/32", "1.0.0.1/1"}, cidrslice{"127.0.0.1/8", "8.8.8.8/32", "1.0.0.1/1"}}, 58 | {cidrslice{"1.1.1./32", "1922.168.42.42/8", "1.1.1.1/33", "1.2.3.4/"}, cidrslice{}}, 59 | {cidrslice{"7.6.5.4/32", "invalid", "1.0.0.1/2"}, cidrslice{"7.6.5.4/32", "1.0.0.1/2"}}, 60 | } { 61 | user, err := DB.Register(test.input) 62 | if err != nil { 63 | t.Errorf("Test %d: Got error from register method: [%v]", i, err) 64 | } 65 | res, err := DB.GetByUsername(user.Username) 66 | if err != nil { 67 | t.Errorf("Test %d: Got error when fetching username: [%v]", i, err) 68 | } 69 | if len(user.AllowFrom) != len(test.output) { 70 | t.Errorf("Test %d: Expected to receive struct with [%d] entries in AllowFrom, but got [%d] records", i, len(test.output), len(user.AllowFrom)) 71 | } 72 | if len(res.AllowFrom) != len(test.output) { 73 | t.Errorf("Test %d: Expected to receive struct with [%d] entries in AllowFrom, but got [%d] records", i, len(test.output), len(res.AllowFrom)) 74 | } 75 | 76 | } 77 | } 78 | 79 | func TestGetByUsername(t *testing.T) { 80 | // Create reg to refer to 81 | reg, err := DB.Register(cidrslice{}) 82 | if err != nil { 83 | t.Errorf("Registration failed, got error [%v]", err) 84 | } 85 | 86 | regUser, err := DB.GetByUsername(reg.Username) 87 | if err != nil { 88 | t.Errorf("Could not get test user, got error [%v]", err) 89 | } 90 | 91 | if reg.Username != regUser.Username { 92 | t.Errorf("GetByUsername username [%q] did not match the original [%q]", regUser.Username, reg.Username) 93 | } 94 | 95 | if reg.Subdomain != regUser.Subdomain { 96 | t.Errorf("GetByUsername subdomain [%q] did not match the original [%q]", regUser.Subdomain, reg.Subdomain) 97 | } 98 | 99 | // regUser password already is a bcrypt hash 100 | if !correctPassword(reg.Password, regUser.Password) { 101 | t.Errorf("The password [%s] does not match the hash [%s]", reg.Password, regUser.Password) 102 | } 103 | } 104 | 105 | func TestPrepareErrors(t *testing.T) { 106 | reg, _ := DB.Register(cidrslice{}) 107 | tdb, err := sql.Open("testdb", "") 108 | if err != nil { 109 | t.Errorf("Got error: %v", err) 110 | } 111 | oldDb := DB.GetBackend() 112 | DB.SetBackend(tdb) 113 | defer DB.SetBackend(oldDb) 114 | defer testdb.Reset() 115 | 116 | _, err = DB.GetByUsername(reg.Username) 117 | if err == nil { 118 | t.Errorf("Expected error, but didn't get one") 119 | } 120 | 121 | _, err = DB.GetTXTForDomain(reg.Subdomain) 122 | if err == nil { 123 | t.Errorf("Expected error, but didn't get one") 124 | } 125 | } 126 | 127 | func TestQueryExecErrors(t *testing.T) { 128 | reg, _ := DB.Register(cidrslice{}) 129 | testdb.SetExecWithArgsFunc(func(query string, args []driver.Value) (result driver.Result, err error) { 130 | return testResult{1, 0}, errors.New("Prepared query error") 131 | }) 132 | 133 | testdb.SetQueryWithArgsFunc(func(query string, args []driver.Value) (result driver.Rows, err error) { 134 | columns := []string{"Username", "Password", "Subdomain", "Value", "LastActive"} 135 | return testdb.RowsFromSlice(columns, [][]driver.Value{}), errors.New("Prepared query error") 136 | }) 137 | 138 | defer testdb.Reset() 139 | 140 | tdb, err := sql.Open("testdb", "") 141 | if err != nil { 142 | t.Errorf("Got error: %v", err) 143 | } 144 | oldDb := DB.GetBackend() 145 | 146 | DB.SetBackend(tdb) 147 | defer DB.SetBackend(oldDb) 148 | 149 | _, err = DB.GetByUsername(reg.Username) 150 | if err == nil { 151 | t.Errorf("Expected error from exec, but got none") 152 | } 153 | 154 | _, err = DB.GetTXTForDomain(reg.Subdomain) 155 | if err == nil { 156 | t.Errorf("Expected error from exec in GetByDomain, but got none") 157 | } 158 | 159 | _, err = DB.Register(cidrslice{}) 160 | if err == nil { 161 | t.Errorf("Expected error from exec in Register, but got none") 162 | } 163 | reg.Value = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 164 | err = DB.Update(reg.ACMETxtPost) 165 | if err == nil { 166 | t.Errorf("Expected error from exec in Update, but got none") 167 | } 168 | 169 | } 170 | 171 | func TestQueryScanErrors(t *testing.T) { 172 | reg, _ := DB.Register(cidrslice{}) 173 | 174 | testdb.SetExecWithArgsFunc(func(query string, args []driver.Value) (result driver.Result, err error) { 175 | return testResult{1, 0}, errors.New("Prepared query error") 176 | }) 177 | 178 | testdb.SetQueryWithArgsFunc(func(query string, args []driver.Value) (result driver.Rows, err error) { 179 | columns := []string{"Only one"} 180 | resultrows := "this value" 181 | return testdb.RowsFromCSVString(columns, resultrows), nil 182 | }) 183 | 184 | defer testdb.Reset() 185 | tdb, err := sql.Open("testdb", "") 186 | if err != nil { 187 | t.Errorf("Got error: %v", err) 188 | } 189 | oldDb := DB.GetBackend() 190 | 191 | DB.SetBackend(tdb) 192 | defer DB.SetBackend(oldDb) 193 | 194 | _, err = DB.GetByUsername(reg.Username) 195 | if err == nil { 196 | t.Errorf("Expected error from scan in, but got none") 197 | } 198 | } 199 | 200 | func TestBadDBValues(t *testing.T) { 201 | reg, _ := DB.Register(cidrslice{}) 202 | 203 | testdb.SetQueryWithArgsFunc(func(query string, args []driver.Value) (result driver.Rows, err error) { 204 | columns := []string{"Username", "Password", "Subdomain", "Value", "LastActive"} 205 | resultrows := "invalid,invalid,invalid,invalid," 206 | return testdb.RowsFromCSVString(columns, resultrows), nil 207 | }) 208 | 209 | defer testdb.Reset() 210 | tdb, err := sql.Open("testdb", "") 211 | if err != nil { 212 | t.Errorf("Got error: %v", err) 213 | } 214 | oldDb := DB.GetBackend() 215 | 216 | DB.SetBackend(tdb) 217 | defer DB.SetBackend(oldDb) 218 | 219 | _, err = DB.GetByUsername(reg.Username) 220 | if err == nil { 221 | t.Errorf("Expected error from scan in, but got none") 222 | } 223 | 224 | _, err = DB.GetTXTForDomain(reg.Subdomain) 225 | if err == nil { 226 | t.Errorf("Expected error from scan in GetByDomain, but got none") 227 | } 228 | } 229 | 230 | func TestGetTXTForDomain(t *testing.T) { 231 | // Create reg to refer to 232 | reg, err := DB.Register(cidrslice{}) 233 | if err != nil { 234 | t.Errorf("Registration failed, got error [%v]", err) 235 | } 236 | 237 | txtval1 := "___validation_token_received_from_the_ca___" 238 | txtval2 := "___validation_token_received_YEAH_the_ca___" 239 | 240 | reg.Value = txtval1 241 | _ = DB.Update(reg.ACMETxtPost) 242 | 243 | reg.Value = txtval2 244 | _ = DB.Update(reg.ACMETxtPost) 245 | 246 | regDomainSlice, err := DB.GetTXTForDomain(reg.Subdomain) 247 | if err != nil { 248 | t.Errorf("Could not get test user, got error [%v]", err) 249 | } 250 | if len(regDomainSlice) == 0 { 251 | t.Errorf("No rows returned for GetTXTForDomain [%s]", reg.Subdomain) 252 | } 253 | 254 | var val1found = false 255 | var val2found = false 256 | for _, v := range regDomainSlice { 257 | if v == txtval1 { 258 | val1found = true 259 | } 260 | if v == txtval2 { 261 | val2found = true 262 | } 263 | } 264 | if !val1found { 265 | t.Errorf("No TXT value found for val1") 266 | } 267 | if !val2found { 268 | t.Errorf("No TXT value found for val2") 269 | } 270 | 271 | // Not found 272 | regNotfound, _ := DB.GetTXTForDomain("does-not-exist") 273 | if len(regNotfound) > 0 { 274 | t.Errorf("No records should be returned.") 275 | } 276 | } 277 | 278 | func TestUpdate(t *testing.T) { 279 | // Create reg to refer to 280 | reg, err := DB.Register(cidrslice{}) 281 | if err != nil { 282 | t.Errorf("Registration failed, got error [%v]", err) 283 | } 284 | 285 | regUser, err := DB.GetByUsername(reg.Username) 286 | if err != nil { 287 | t.Errorf("Could not get test user, got error [%v]", err) 288 | } 289 | 290 | // Set new values (only TXT should be updated) (matches by username and subdomain) 291 | 292 | validTXT := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 293 | 294 | regUser.Password = "nevergonnagiveyouup" 295 | regUser.Value = validTXT 296 | 297 | err = DB.Update(regUser.ACMETxtPost) 298 | if err != nil { 299 | t.Errorf("DB Update failed, got error: [%v]", err) 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /dns.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/miekg/dns" 6 | log "github.com/sirupsen/logrus" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // Records is a slice of ResourceRecords 12 | type Records struct { 13 | Records []dns.RR 14 | } 15 | 16 | // DNSServer is the main struct for acme-dns DNS server 17 | type DNSServer struct { 18 | DB database 19 | Domain string 20 | Server *dns.Server 21 | SOA dns.RR 22 | PersonalKeyAuth string 23 | Domains map[string]Records 24 | } 25 | 26 | // NewDNSServer parses the DNS records from config and returns a new DNSServer struct 27 | func NewDNSServer(db database, addr string, proto string, domain string) *DNSServer { 28 | var server DNSServer 29 | server.Server = &dns.Server{Addr: addr, Net: proto} 30 | if !strings.HasSuffix(domain, ".") { 31 | domain = domain + "." 32 | } 33 | server.Domain = strings.ToLower(domain) 34 | server.DB = db 35 | server.PersonalKeyAuth = "" 36 | server.Domains = make(map[string]Records) 37 | return &server 38 | } 39 | 40 | // Start starts the DNSServer 41 | func (d *DNSServer) Start(errorChannel chan error) { 42 | // DNS server part 43 | dns.HandleFunc(".", d.handleRequest) 44 | log.WithFields(log.Fields{"addr": d.Server.Addr, "proto": d.Server.Net}).Info("Listening DNS") 45 | err := d.Server.ListenAndServe() 46 | if err != nil { 47 | errorChannel <- err 48 | } 49 | } 50 | 51 | // ParseRecords parses a slice of DNS record string 52 | func (d *DNSServer) ParseRecords(config DNSConfig) { 53 | for _, v := range config.General.StaticRecords { 54 | rr, err := dns.NewRR(strings.ToLower(v)) 55 | if err != nil { 56 | log.WithFields(log.Fields{"error": err.Error(), "rr": v}).Warning("Could not parse RR from config") 57 | continue 58 | } 59 | // Add parsed RR 60 | d.appendRR(rr) 61 | } 62 | // Create serial 63 | serial := time.Now().Format("2006010215") 64 | // Add SOA 65 | 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) 66 | soarr, err := dns.NewRR(SOAstring) 67 | if err != nil { 68 | log.WithFields(log.Fields{"error": err.Error(), "soa": SOAstring}).Error("Error while adding SOA record") 69 | } else { 70 | d.appendRR(soarr) 71 | d.SOA = soarr 72 | } 73 | } 74 | 75 | func (d *DNSServer) appendRR(rr dns.RR) { 76 | addDomain := rr.Header().Name 77 | _, ok := d.Domains[addDomain] 78 | if !ok { 79 | d.Domains[addDomain] = Records{[]dns.RR{rr}} 80 | } else { 81 | drecs := d.Domains[addDomain] 82 | drecs.Records = append(drecs.Records, rr) 83 | d.Domains[addDomain] = drecs 84 | } 85 | log.WithFields(log.Fields{"recordtype": dns.TypeToString[rr.Header().Rrtype], "domain": addDomain}).Debug("Adding new record to domain") 86 | } 87 | 88 | func (d *DNSServer) handleRequest(w dns.ResponseWriter, r *dns.Msg) { 89 | m := new(dns.Msg) 90 | m.SetReply(r) 91 | 92 | // handle edns0 93 | opt := r.IsEdns0() 94 | if opt != nil { 95 | if opt.Version() != 0 { 96 | // Only EDNS0 is standardized 97 | m.MsgHdr.Rcode = dns.RcodeBadVers 98 | m.SetEdns0(512, false) 99 | } else { 100 | // We can safely do this as we know that we're not setting other OPT RRs within acme-dns. 101 | m.SetEdns0(512, false) 102 | if r.Opcode == dns.OpcodeQuery { 103 | d.readQuery(m) 104 | } 105 | } 106 | } else { 107 | if r.Opcode == dns.OpcodeQuery { 108 | d.readQuery(m) 109 | } 110 | } 111 | _ = w.WriteMsg(m) 112 | } 113 | 114 | func (d *DNSServer) readQuery(m *dns.Msg) { 115 | var authoritative = false 116 | for _, que := range m.Question { 117 | if rr, rc, auth, err := d.answer(que); err == nil { 118 | if auth { 119 | authoritative = auth 120 | } 121 | m.MsgHdr.Rcode = rc 122 | m.Answer = append(m.Answer, rr...) 123 | } 124 | } 125 | m.MsgHdr.Authoritative = authoritative 126 | if authoritative { 127 | if m.MsgHdr.Rcode == dns.RcodeNameError { 128 | m.Ns = append(m.Ns, d.SOA) 129 | } 130 | } 131 | } 132 | 133 | func (d *DNSServer) getRecord(q dns.Question) ([]dns.RR, error) { 134 | var rr []dns.RR 135 | var cnames []dns.RR 136 | domain, ok := d.Domains[strings.ToLower(q.Name)] 137 | if !ok { 138 | return rr, fmt.Errorf("No records for domain %s", q.Name) 139 | } 140 | for _, ri := range domain.Records { 141 | if ri.Header().Rrtype == q.Qtype { 142 | rr = append(rr, ri) 143 | } 144 | if ri.Header().Rrtype == dns.TypeCNAME { 145 | cnames = append(cnames, ri) 146 | } 147 | } 148 | if len(rr) == 0 { 149 | return cnames, nil 150 | } 151 | return rr, nil 152 | } 153 | 154 | // answeringForDomain checks if we have any records for a domain 155 | func (d *DNSServer) answeringForDomain(name string) bool { 156 | if d.Domain == strings.ToLower(name) { 157 | return true 158 | } 159 | _, ok := d.Domains[strings.ToLower(name)] 160 | return ok 161 | } 162 | 163 | func (d *DNSServer) isAuthoritative(q dns.Question) bool { 164 | if d.answeringForDomain(q.Name) { 165 | return true 166 | } 167 | domainParts := strings.Split(strings.ToLower(q.Name), ".") 168 | for i := range domainParts { 169 | if d.answeringForDomain(strings.Join(domainParts[i:], ".")) { 170 | return true 171 | } 172 | } 173 | return false 174 | } 175 | 176 | // isOwnChallenge checks if the query is for the domain of this acme-dns instance. Used for answering its own ACME challenges 177 | func (d *DNSServer) isOwnChallenge(name string) bool { 178 | domainParts := strings.SplitN(name, ".", 2) 179 | if len(domainParts) == 2 { 180 | if strings.ToLower(domainParts[0]) == "_acme-challenge" { 181 | domain := strings.ToLower(domainParts[1]) 182 | if !strings.HasSuffix(domain, ".") { 183 | domain = domain + "." 184 | } 185 | if domain == d.Domain { 186 | return true 187 | } 188 | } 189 | } 190 | return false 191 | } 192 | 193 | func (d *DNSServer) answer(q dns.Question) ([]dns.RR, int, bool, error) { 194 | var rcode int 195 | var err error 196 | var txtRRs []dns.RR 197 | var authoritative = d.isAuthoritative(q) 198 | if !d.isOwnChallenge(q.Name) && !d.answeringForDomain(q.Name) { 199 | rcode = dns.RcodeNameError 200 | } 201 | r, _ := d.getRecord(q) 202 | if q.Qtype == dns.TypeTXT { 203 | if d.isOwnChallenge(q.Name) { 204 | txtRRs, err = d.answerOwnChallenge(q) 205 | } else { 206 | txtRRs, err = d.answerTXT(q) 207 | } 208 | if err == nil { 209 | r = append(r, txtRRs...) 210 | } 211 | } 212 | if len(r) > 0 { 213 | // Make sure that we return NOERROR if there were dynamic records for the domain 214 | rcode = dns.RcodeSuccess 215 | } 216 | log.WithFields(log.Fields{"qtype": dns.TypeToString[q.Qtype], "domain": q.Name, "rcode": dns.RcodeToString[rcode]}).Debug("Answering question for domain") 217 | return r, rcode, authoritative, nil 218 | } 219 | 220 | func (d *DNSServer) answerTXT(q dns.Question) ([]dns.RR, error) { 221 | var ra []dns.RR 222 | subdomain := sanitizeDomainQuestion(q.Name) 223 | atxt, err := d.DB.GetTXTForDomain(subdomain) 224 | if err != nil { 225 | log.WithFields(log.Fields{"error": err.Error()}).Debug("Error while trying to get record") 226 | return ra, err 227 | } 228 | for _, v := range atxt { 229 | if len(v) > 0 { 230 | r := new(dns.TXT) 231 | r.Hdr = dns.RR_Header{Name: q.Name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 1} 232 | r.Txt = append(r.Txt, v) 233 | ra = append(ra, r) 234 | } 235 | } 236 | return ra, nil 237 | } 238 | 239 | // answerOwnChallenge answers to ACME challenge for acme-dns own certificate 240 | func (d *DNSServer) answerOwnChallenge(q dns.Question) ([]dns.RR, error) { 241 | r := new(dns.TXT) 242 | r.Hdr = dns.RR_Header{Name: q.Name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 1} 243 | r.Txt = append(r.Txt, d.PersonalKeyAuth) 244 | return []dns.RR{r}, nil 245 | } 246 | -------------------------------------------------------------------------------- /dns_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "database/sql/driver" 6 | "errors" 7 | "fmt" 8 | "testing" 9 | 10 | "github.com/erikstmartin/go-testdb" 11 | "github.com/miekg/dns" 12 | ) 13 | 14 | type resolver struct { 15 | server string 16 | } 17 | 18 | func (r *resolver) lookup(host string, qtype uint16) (*dns.Msg, error) { 19 | msg := new(dns.Msg) 20 | msg.Id = dns.Id() 21 | msg.Question = make([]dns.Question, 1) 22 | msg.Question[0] = dns.Question{Name: dns.Fqdn(host), Qtype: qtype, Qclass: dns.ClassINET} 23 | in, err := dns.Exchange(msg, r.server) 24 | if err != nil { 25 | return in, fmt.Errorf("Error querying the server [%v]", err) 26 | } 27 | if in != nil && in.Rcode != dns.RcodeSuccess { 28 | return in, fmt.Errorf("Received error from the server [%s]", dns.RcodeToString[in.Rcode]) 29 | } 30 | 31 | return in, nil 32 | } 33 | 34 | func hasExpectedTXTAnswer(answer []dns.RR, cmpTXT string) error { 35 | for _, record := range answer { 36 | // We expect only one answer, so no need to loop through the answer slice 37 | if rec, ok := record.(*dns.TXT); ok { 38 | for _, txtValue := range rec.Txt { 39 | if txtValue == cmpTXT { 40 | return nil 41 | } 42 | } 43 | } else { 44 | errmsg := fmt.Sprintf("Got answer of unexpected type [%q]", answer[0]) 45 | return errors.New(errmsg) 46 | } 47 | } 48 | return errors.New("Expected answer not found") 49 | } 50 | 51 | func TestQuestionDBError(t *testing.T) { 52 | testdb.SetQueryWithArgsFunc(func(query string, args []driver.Value) (result driver.Rows, err error) { 53 | columns := []string{"Username", "Password", "Subdomain", "Value", "LastActive"} 54 | return testdb.RowsFromSlice(columns, [][]driver.Value{}), errors.New("Prepared query error") 55 | }) 56 | 57 | defer testdb.Reset() 58 | 59 | tdb, err := sql.Open("testdb", "") 60 | if err != nil { 61 | t.Errorf("Got error: %v", err) 62 | } 63 | oldDb := DB.GetBackend() 64 | 65 | DB.SetBackend(tdb) 66 | defer DB.SetBackend(oldDb) 67 | 68 | q := dns.Question{Name: dns.Fqdn("whatever.tld"), Qtype: dns.TypeTXT, Qclass: dns.ClassINET} 69 | _, err = dnsserver.answerTXT(q) 70 | if err == nil { 71 | t.Errorf("Expected error but got none") 72 | } 73 | } 74 | 75 | func TestParse(t *testing.T) { 76 | var testcfg = DNSConfig{ 77 | General: general{ 78 | Domain: ")", 79 | Nsname: "ns1.auth.example.org", 80 | Nsadmin: "admin.example.org", 81 | StaticRecords: []string{}, 82 | Debug: false, 83 | }, 84 | } 85 | dnsserver.ParseRecords(testcfg) 86 | if !loggerHasEntryWithMessage("Error while adding SOA record") { 87 | t.Errorf("Expected SOA parsing to return error, but did not find one") 88 | } 89 | } 90 | 91 | func TestResolveA(t *testing.T) { 92 | resolv := resolver{server: "127.0.0.1:15353"} 93 | answer, err := resolv.lookup("auth.example.org", dns.TypeA) 94 | if err != nil { 95 | t.Errorf("%v", err) 96 | } 97 | 98 | if len(answer.Answer) == 0 { 99 | t.Error("No answer for DNS query") 100 | } 101 | 102 | _, err = resolv.lookup("nonexistent.domain.tld", dns.TypeA) 103 | if err == nil { 104 | t.Errorf("Was expecting error because of NXDOMAIN but got none") 105 | } 106 | } 107 | 108 | func TestEDNS(t *testing.T) { 109 | resolv := resolver{server: "127.0.0.1:15353"} 110 | answer, _ := resolv.lookup("auth.example.org", dns.TypeOPT) 111 | if answer.Rcode != dns.RcodeSuccess { 112 | t.Errorf("Was expecing NOERROR rcode for OPT query, but got [%s] instead.", dns.RcodeToString[answer.Rcode]) 113 | } 114 | } 115 | 116 | func TestEDNSA(t *testing.T) { 117 | msg := new(dns.Msg) 118 | msg.Id = dns.Id() 119 | msg.Question = make([]dns.Question, 1) 120 | msg.Question[0] = dns.Question{Name: dns.Fqdn("auth.example.org"), Qtype: dns.TypeA, Qclass: dns.ClassINET} 121 | // Set EDNS0 with DO=1 122 | msg.SetEdns0(512, true) 123 | in, err := dns.Exchange(msg, "127.0.0.1:15353") 124 | if err != nil { 125 | t.Errorf("Error querying the server [%v]", err) 126 | } 127 | if in != nil && in.Rcode != dns.RcodeSuccess { 128 | t.Errorf("Received error from the server [%s]", dns.RcodeToString[in.Rcode]) 129 | } 130 | opt := in.IsEdns0() 131 | if opt == nil { 132 | t.Errorf("Should have got OPT back") 133 | } 134 | } 135 | 136 | func TestEDNSBADVERS(t *testing.T) { 137 | msg := new(dns.Msg) 138 | msg.Id = dns.Id() 139 | msg.Question = make([]dns.Question, 1) 140 | msg.Question[0] = dns.Question{Name: dns.Fqdn("auth.example.org"), Qtype: dns.TypeA, Qclass: dns.ClassINET} 141 | // Set EDNS0 with version 1 142 | o := new(dns.OPT) 143 | o.SetVersion(1) 144 | o.Hdr.Name = "." 145 | o.Hdr.Rrtype = dns.TypeOPT 146 | msg.Extra = append(msg.Extra, o) 147 | in, err := dns.Exchange(msg, "127.0.0.1:15353") 148 | if err != nil { 149 | t.Errorf("Error querying the server [%v]", err) 150 | } 151 | if in != nil && in.Rcode != dns.RcodeBadVers { 152 | t.Errorf("Received unexpected rcode from the server [%s]", dns.RcodeToString[in.Rcode]) 153 | } 154 | } 155 | 156 | func TestResolveCNAME(t *testing.T) { 157 | resolv := resolver{server: "127.0.0.1:15353"} 158 | expected := "cn.example.org. 3600 IN CNAME something.example.org." 159 | answer, err := resolv.lookup("cn.example.org", dns.TypeCNAME) 160 | if err != nil { 161 | t.Errorf("Got unexpected error: %s", err) 162 | } 163 | if len(answer.Answer) != 1 { 164 | t.Errorf("Expected exactly 1 RR in answer, but got %d instead.", len(answer.Answer)) 165 | } 166 | if answer.Answer[0].Header().Rrtype != dns.TypeCNAME { 167 | t.Errorf("Expected a CNAME answer, but got [%s] instead.", dns.TypeToString[answer.Answer[0].Header().Rrtype]) 168 | } 169 | if answer.Answer[0].String() != expected { 170 | t.Errorf("Expected CNAME answer [%s] but got [%s] instead.", expected, answer.Answer[0].String()) 171 | } 172 | } 173 | 174 | func TestAuthoritative(t *testing.T) { 175 | resolv := resolver{server: "127.0.0.1:15353"} 176 | answer, _ := resolv.lookup("nonexistent.auth.example.org", dns.TypeA) 177 | if answer.Rcode != dns.RcodeNameError { 178 | t.Errorf("Was expecing NXDOMAIN rcode, but got [%s] instead.", dns.RcodeToString[answer.Rcode]) 179 | } 180 | if len(answer.Ns) != 1 { 181 | t.Errorf("Was expecting exactly one answer (SOA) for invalid subdomain, but got %d", len(answer.Ns)) 182 | } 183 | if answer.Ns[0].Header().Rrtype != dns.TypeSOA { 184 | t.Errorf("Was expecting SOA record as answer for NXDOMAIN but got [%s]", dns.TypeToString[answer.Ns[0].Header().Rrtype]) 185 | } 186 | if !answer.MsgHdr.Authoritative { 187 | t.Errorf("Was expecting authoritative bit to be set") 188 | } 189 | nanswer, _ := resolv.lookup("nonexsitent.nonauth.tld", dns.TypeA) 190 | if len(nanswer.Answer) > 0 { 191 | t.Errorf("Didn't expect answers for non authotitative domain query") 192 | } 193 | if nanswer.MsgHdr.Authoritative { 194 | t.Errorf("Authoritative bit should not be set for non-authoritative domain.") 195 | } 196 | } 197 | 198 | func TestResolveTXT(t *testing.T) { 199 | resolv := resolver{server: "127.0.0.1:15353"} 200 | validTXT := "______________valid_response_______________" 201 | 202 | atxt, err := DB.Register(cidrslice{}) 203 | if err != nil { 204 | t.Errorf("Could not initiate db record: [%v]", err) 205 | return 206 | } 207 | atxt.Value = validTXT 208 | err = DB.Update(atxt.ACMETxtPost) 209 | if err != nil { 210 | t.Errorf("Could not update db record: [%v]", err) 211 | return 212 | } 213 | 214 | for i, test := range []struct { 215 | subDomain string 216 | expTXT string 217 | getAnswer bool 218 | validAnswer bool 219 | }{ 220 | {atxt.Subdomain, validTXT, true, true}, 221 | {atxt.Subdomain, "invalid", true, false}, 222 | {"a097455b-52cc-4569-90c8-7a4b97c6eba8", validTXT, false, false}, 223 | } { 224 | answer, err := resolv.lookup(test.subDomain+".auth.example.org", dns.TypeTXT) 225 | if err != nil { 226 | if test.getAnswer { 227 | t.Fatalf("Test %d: Expected answer but got: %v", i, err) 228 | } 229 | } else { 230 | if !test.getAnswer { 231 | t.Errorf("Test %d: Expected no answer, but got one.", i) 232 | } 233 | } 234 | 235 | if len(answer.Answer) > 0 { 236 | if !test.getAnswer && answer.Answer[0].Header().Rrtype != dns.TypeSOA { 237 | t.Errorf("Test %d: Expected no answer, but got: [%q]", i, answer) 238 | } 239 | if test.getAnswer { 240 | err = hasExpectedTXTAnswer(answer.Answer, test.expTXT) 241 | if err != nil { 242 | if test.validAnswer { 243 | t.Errorf("Test %d: %v", i, err) 244 | } 245 | } else { 246 | if !test.validAnswer { 247 | t.Errorf("Test %d: Answer was not expected to be valid, answer [%q], compared to [%s]", i, answer, test.expTXT) 248 | } 249 | } 250 | } 251 | } else { 252 | if test.getAnswer { 253 | t.Errorf("Test %d: Expected answer, but didn't get one", i) 254 | } 255 | } 256 | } 257 | } 258 | 259 | func TestCaseInsensitiveResolveA(t *testing.T) { 260 | resolv := resolver{server: "127.0.0.1:15353"} 261 | answer, err := resolv.lookup("aUtH.eXAmpLe.org", dns.TypeA) 262 | if err != nil { 263 | t.Errorf("%v", err) 264 | } 265 | 266 | if len(answer.Answer) == 0 { 267 | t.Error("No answer for DNS query") 268 | } 269 | } 270 | 271 | func TestCaseInsensitiveResolveSOA(t *testing.T) { 272 | resolv := resolver{server: "127.0.0.1:15353"} 273 | answer, _ := resolv.lookup("doesnotexist.aUtH.eXAmpLe.org", dns.TypeSOA) 274 | if answer.Rcode != dns.RcodeNameError { 275 | t.Errorf("Was expecing NXDOMAIN rcode, but got [%s] instead.", dns.RcodeToString[answer.Rcode]) 276 | } 277 | 278 | if len(answer.Ns) == 0 { 279 | t.Error("No SOA answer for DNS query") 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | acmedns: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | image: joohoi/acme-dns:latest 8 | ports: 9 | - "443:443" 10 | - "53:53" 11 | - "53:53/udp" 12 | - "80:80" 13 | volumes: 14 | - ./config:/etc/acme-dns:ro 15 | - ./data:/var/lib/acme-dns 16 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/joohoi/acme-dns 2 | 3 | go 1.22 4 | toolchain go1.22.0 5 | 6 | require ( 7 | github.com/BurntSushi/toml v1.4.0 8 | github.com/DATA-DOG/go-sqlmock v1.5.0 9 | github.com/caddyserver/certmagic v0.21.4 10 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 11 | github.com/gavv/httpexpect v2.0.0+incompatible 12 | github.com/go-acme/lego/v3 v3.9.0 13 | github.com/google/uuid v1.6.0 14 | github.com/julienschmidt/httprouter v1.3.0 15 | github.com/lib/pq v1.10.9 16 | github.com/mattn/go-sqlite3 v1.14.24 17 | github.com/mholt/acmez/v2 v2.0.3 18 | github.com/miekg/dns v1.1.62 19 | github.com/rs/cors v1.11.1 20 | github.com/sirupsen/logrus v1.9.3 21 | golang.org/x/crypto v0.31.0 22 | ) 23 | 24 | require ( 25 | github.com/ajg/form v1.5.1 // indirect 26 | github.com/andybalholm/brotli v1.0.2 // indirect 27 | github.com/caddyserver/zerossl v0.1.3 // indirect 28 | github.com/davecgh/go-spew v1.1.1 // indirect 29 | github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072 // indirect 30 | github.com/fatih/structs v1.1.0 // indirect 31 | github.com/google/go-querystring v1.0.0 // indirect 32 | github.com/gorilla/websocket v1.4.2 // indirect 33 | github.com/imkira/go-interpol v1.1.0 // indirect 34 | github.com/klauspost/compress v1.13.4 // indirect 35 | github.com/klauspost/cpuid/v2 v2.2.9 // indirect 36 | github.com/libdns/libdns v0.2.2 // indirect 37 | github.com/mattn/go-colorable v0.1.12 // indirect 38 | github.com/moul/http2curl v1.0.0 // indirect 39 | github.com/pmezard/go-difflib v1.0.0 // indirect 40 | github.com/sergi/go-diff v1.2.0 // indirect 41 | github.com/stretchr/testify v1.8.1 // indirect 42 | github.com/valyala/bytebufferpool v1.0.0 // indirect 43 | github.com/valyala/fasthttp v1.31.0 // indirect 44 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect 45 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 46 | github.com/xeipuuv/gojsonschema v1.2.0 // indirect 47 | github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect 48 | github.com/yudai/gojsondiff v1.0.0 // indirect 49 | github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect 50 | github.com/yudai/pp v2.0.1+incompatible // indirect 51 | github.com/zeebo/blake3 v0.2.4 // indirect 52 | go.uber.org/multierr v1.11.0 // indirect 53 | go.uber.org/zap v1.27.0 // indirect 54 | golang.org/x/mod v0.22.0 // indirect 55 | golang.org/x/net v0.32.0 // indirect 56 | golang.org/x/sync v0.10.0 // indirect 57 | golang.org/x/sys v0.28.0 // indirect 58 | golang.org/x/text v0.21.0 // indirect 59 | golang.org/x/tools v0.28.0 // indirect 60 | gopkg.in/yaml.v3 v3.0.1 // indirect 61 | ) 62 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 9 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 10 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 11 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 12 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 13 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 14 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 15 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 16 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 17 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 18 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 19 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 20 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 21 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 22 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 23 | contrib.go.opencensus.io/exporter/ocagent v0.4.12/go.mod h1:450APlNTSR6FrvC3CTRqYosuDstRB9un7SOx2k/9ckA= 24 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 25 | github.com/Azure/azure-sdk-for-go v32.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= 26 | github.com/Azure/go-autorest/autorest v0.1.0/go.mod h1:AKyIcETwSUFxIcs/Wnq/C+kwCtlEYGUVd7FPNb2slmg= 27 | github.com/Azure/go-autorest/autorest v0.5.0/go.mod h1:9HLKlQjVBH6U3oDfsXOeVc56THsLPw1L03yban4xThw= 28 | github.com/Azure/go-autorest/autorest/adal v0.1.0/go.mod h1:MeS4XhScH55IST095THyTxElntu7WqB7pNbZo8Q5G3E= 29 | github.com/Azure/go-autorest/autorest/adal v0.2.0/go.mod h1:MeS4XhScH55IST095THyTxElntu7WqB7pNbZo8Q5G3E= 30 | github.com/Azure/go-autorest/autorest/azure/auth v0.1.0/go.mod h1:Gf7/i2FUpyb/sGBLIFxTBzrNzBo7aPXXE3ZVeDRwdpM= 31 | github.com/Azure/go-autorest/autorest/azure/cli v0.1.0/go.mod h1:Dk8CUAt/b/PzkfeRsWzVG9Yj3ps8mS8ECztu43rdU8U= 32 | github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= 33 | github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= 34 | github.com/Azure/go-autorest/autorest/to v0.2.0/go.mod h1:GunWKJp1AEqgMaGLV+iocmRAJWqST1wQYhyyjXJ3SJc= 35 | github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQvokg3NZAlQTalVMtOIAs1aGK7G6u8= 36 | github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= 37 | github.com/Azure/go-autorest/tracing v0.1.0/go.mod h1:ROEEAFwXycQw7Sn3DXNtEedEvdeRAgDr0izn4z5Ij88= 38 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 39 | github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= 40 | github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 41 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 42 | github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= 43 | github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= 44 | github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87/go.mod h1:iGLljf5n9GjT6kc0HBvyI1nOKnGQbNB66VzSNbK5iks= 45 | github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= 46 | github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= 47 | github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= 48 | github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= 49 | github.com/akamai/AkamaiOPEN-edgegrid-golang v0.9.18/go.mod h1:L+HB2uBoDgi3+r1pJEJcbGwyyHhd2QXaGsKLbDwtm8Q= 50 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 51 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 52 | github.com/aliyun/alibaba-cloud-sdk-go v1.61.112/go.mod h1:pUKYbK5JQ+1Dfxk80P0qxGqe5dkxDoabbZS7zOcouyA= 53 | github.com/andybalholm/brotli v1.0.2 h1:JKnhI/XQ75uFBTiuzXpzFrUriDPiZjlOSzh6wXogP0E= 54 | github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= 55 | github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= 56 | github.com/aws/aws-sdk-go v1.30.20/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= 57 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 58 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 59 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 60 | github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= 61 | github.com/caddyserver/certmagic v0.21.4 h1:e7VobB8rffHv8ZZpSiZtEwnLDHUwLVYLWzWSa1FfKI0= 62 | github.com/caddyserver/certmagic v0.21.4/go.mod h1:swUXjQ1T9ZtMv95qj7/InJvWLXURU85r+CfG0T+ZbDE= 63 | github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA= 64 | github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= 65 | github.com/cenkalti/backoff/v4 v4.0.0/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg= 66 | github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 67 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 68 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 69 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 70 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 71 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 72 | github.com/cloudflare/cloudflare-go v0.10.2/go.mod h1:qhVI5MKwBGhdNU89ZRz2plgYutcJ5PCekLxXn56w6SY= 73 | github.com/cpu/goacmedns v0.0.2/go.mod h1:4MipLkI+qScwqtVxcNO6okBhbgRrr7/tKXUSgSL0teQ= 74 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 75 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 76 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 77 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 78 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 79 | github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= 80 | github.com/dnaeon/go-vcr v0.0.0-20180814043457-aafff18a5cc2/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= 81 | github.com/dnsimple/dnsimple-go v0.60.0/go.mod h1:O5TJ0/U6r7AfT8niYNlmohpLbCSG+c71tQlGr9SeGrg= 82 | github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= 83 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= 84 | github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= 85 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 86 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 87 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= 88 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= 89 | github.com/exoscale/egoscale v0.18.1/go.mod h1:Z7OOdzzTOz1Q1PjQXumlz9Wn/CddH0zSYdCF3rnBKXE= 90 | github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072 h1:DddqAaWDpywytcG8w/qoQ5sAN8X12d3Z3koB0C3Rxsc= 91 | github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= 92 | github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= 93 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 94 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 95 | github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8= 96 | github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= 97 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 98 | github.com/go-acme/lego/v3 v3.9.0 h1:Kyvg2GGqRJHfK2Stu57M45TDTx0y1bsxLH7lpeP3n0A= 99 | github.com/go-acme/lego/v3 v3.9.0/go.mod h1:va0cvQpxpJ3u2OA534L8TDn+lsr2oujLzPckLOLnUGQ= 100 | github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s= 101 | github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= 102 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 103 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 104 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 105 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 106 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 107 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 108 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 109 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 110 | github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 111 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 112 | github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 113 | github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= 114 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 115 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 116 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 117 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 118 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 119 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 120 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 121 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 122 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 123 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 124 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 125 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 126 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 127 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 128 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 129 | github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 130 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 131 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 132 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 133 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 134 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 135 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 136 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 137 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 138 | github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= 139 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 140 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 141 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 142 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 143 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 144 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 145 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 146 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 147 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 148 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 149 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 150 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 151 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 152 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 153 | github.com/gophercloud/gophercloud v0.6.1-0.20191122030953-d8ac278c1c9d/go.mod h1:ozGNgr9KYOVATV5jsgHl/ceCDXGuguqOZAzoQ/2vcNM= 154 | github.com/gophercloud/gophercloud v0.7.0/go.mod h1:gmC5oQqMDOMO1t1gq5DquX/yAU808e/4mzjjDA76+Ss= 155 | github.com/gophercloud/utils v0.0.0-20200508015959-b0167b94122c/go.mod h1:ehWUbLQJPqS0Ep+CxeD559hsm9pthPXadJNKwZkp43w= 156 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 157 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 158 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 159 | github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 160 | github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 161 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 162 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 163 | github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 164 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= 165 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 166 | github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= 167 | github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= 168 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 169 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 170 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 171 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 172 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 173 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 174 | github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhKWFeDesPjMj+wCHReeknARU3wqlyN4= 175 | github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= 176 | github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= 177 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 178 | github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= 179 | github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 180 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 181 | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 182 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 183 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 184 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 185 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 186 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 187 | github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= 188 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 189 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 190 | github.com/klauspost/compress v1.13.4 h1:0zhec2I8zGnjWcKyLl6i3gPqKANCCn5e9xmviEEeX6s= 191 | github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= 192 | github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= 193 | github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= 194 | github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ= 195 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 196 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 197 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 198 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 199 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 200 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 201 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 202 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 203 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 204 | github.com/labbsr0x/bindman-dns-webhook v1.0.2/go.mod h1:p6b+VCXIR8NYKpDr8/dg1HKfQoRHCdcsROXKvmoehKA= 205 | github.com/labbsr0x/goh v1.0.1/go.mod h1:8K2UhVoaWXcCU7Lxoa2omWnC8gyW8px7/lmO61c027w= 206 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 207 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 208 | github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s= 209 | github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= 210 | github.com/linode/linodego v0.10.0/go.mod h1:cziNP7pbvE3mXIPneHj0oRY8L1WtGEIKlZ8LANE4eXA= 211 | github.com/liquidweb/liquidweb-go v1.6.0/go.mod h1:UDcVnAMDkZxpw4Y7NOHkqoeiGacVLEIG/i5J9cyixzQ= 212 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= 213 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 214 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 215 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 216 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 217 | github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 218 | github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 219 | github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= 220 | github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 221 | github.com/mattn/go-tty v0.0.0-20180219170247-931426f7535a/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE= 222 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 223 | github.com/mholt/acmez/v2 v2.0.3 h1:CgDBlEwg3QBp6s45tPQmFIBrkRIkBT4rW4orMM6p4sw= 224 | github.com/mholt/acmez/v2 v2.0.3/go.mod h1:pQ1ysaDeGrIMvJ9dfJMk5kJNkn7L2sb3UhyrX6Q91cw= 225 | github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= 226 | github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= 227 | github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= 228 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 229 | github.com/mitchellh/go-vnc v0.0.0-20150629162542-723ed9867aed/go.mod h1:3rdaFaCv4AyBgu5ALFM0+tSuHrBh6v692nyQe3ikrq0= 230 | github.com/mitchellh/mapstructure v1.3.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 231 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 232 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 233 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 234 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 235 | github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs= 236 | github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= 237 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 238 | github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8= 239 | github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= 240 | github.com/nrdcg/auroradns v1.0.1/go.mod h1:y4pc0i9QXYlFCWrhWrUSIETnZgrf4KuwjDIWmmXo3JI= 241 | github.com/nrdcg/desec v0.5.0/go.mod h1:2ejvMazkav1VdDbv2HeQO7w+Ta1CGHqzQr27ZBYTuEQ= 242 | github.com/nrdcg/dnspod-go v0.4.0/go.mod h1:vZSoFSFeQVm2gWLMkyX61LZ8HI3BaqtHZWgPTGKr6KQ= 243 | github.com/nrdcg/goinwx v0.7.0/go.mod h1:4tKJOCi/1lTxuw9/yB2Ez0aojwtUCSkckjc22eALpqE= 244 | github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw= 245 | github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= 246 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 247 | github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= 248 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 249 | github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= 250 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 251 | github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= 252 | github.com/oracle/oci-go-sdk v7.0.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888= 253 | github.com/ovh/go-ovh v0.0.0-20181109152953-ba5adb4cf014/go.mod h1:joRatxRJaZBsY3JAOEMcoOp05CnZzsx4scTxi95DHyQ= 254 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 255 | github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 256 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 257 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 258 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 259 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 260 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 261 | github.com/pquerna/otp v1.2.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= 262 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 263 | github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= 264 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 265 | github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= 266 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 267 | github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 268 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 269 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 270 | github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 271 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 272 | github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= 273 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 274 | github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 275 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 276 | github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= 277 | github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2/go.mod h1:7tZKcyumwBO6qip7RNQ5r77yrssm9bfCowcLEBcU5IA= 278 | github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 279 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 280 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 281 | github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= 282 | github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= 283 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 284 | github.com/sacloud/libsacloud v1.26.1/go.mod h1:79ZwATmHLIFZIMd7sxA3LwzVy/B77uj3LDoToVTxDoQ= 285 | github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= 286 | github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 287 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 288 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 289 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 290 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 291 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 292 | github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= 293 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 294 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 295 | github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 296 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 297 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 298 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 299 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 300 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 301 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 302 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 303 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 304 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 305 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 306 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 307 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 308 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 309 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 310 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 311 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 312 | github.com/timewasted/linode v0.0.0-20160829202747-37e84520dcf7/go.mod h1:imsgLplxEC/etjIhdr3dNzV3JeT27LbVu5pYWm0JCBY= 313 | github.com/transip/gotransip/v6 v6.0.2/go.mod h1:pQZ36hWWRahCUXkFWlx9Hs711gLd8J4qdgLdRzmtY+g= 314 | github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= 315 | github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 316 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 317 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 318 | github.com/valyala/fasthttp v1.31.0 h1:lrauRLII19afgCs2fnWRJ4M5IkV0lo2FqA61uGkNBfE= 319 | github.com/valyala/fasthttp v1.31.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus= 320 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 321 | github.com/vultr/govultr v0.4.2/go.mod h1:TUuUizMOFc7z+PNMssb6iGjKjQfpw5arIaOLfocVudQ= 322 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= 323 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 324 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= 325 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 326 | github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= 327 | github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= 328 | github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= 329 | github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= 330 | github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= 331 | github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= 332 | github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= 333 | github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= 334 | github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI= 335 | github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= 336 | github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= 337 | github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= 338 | github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= 339 | github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= 340 | github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= 341 | github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= 342 | go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= 343 | go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= 344 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 345 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 346 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 347 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 348 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 349 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 350 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 351 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 352 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 353 | go.uber.org/ratelimit v0.0.0-20180316092928-c15da0234277/go.mod h1:2X8KaoNd1J0lZV+PxJk/5+DGbO/tpwLR1m++a7FnB/Y= 354 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 355 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 356 | golang.org/x/crypto v0.0.0-20180621125126-a49355c7e3f8/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 357 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 358 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 359 | golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 360 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 361 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 362 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 363 | golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 364 | golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 365 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= 366 | golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 367 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 368 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 369 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 370 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 371 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 372 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 373 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 374 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 375 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 376 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 377 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 378 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 379 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 380 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 381 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 382 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 383 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 384 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 385 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 386 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 387 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 388 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 389 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 390 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 391 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 392 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 393 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 394 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 395 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 396 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 397 | golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= 398 | golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 399 | golang.org/x/net v0.0.0-20180611182652-db08ff08e862/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 400 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 401 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 402 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 403 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 404 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 405 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 406 | golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 407 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 408 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 409 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 410 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 411 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 412 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 413 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 414 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 415 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 416 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 417 | golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 418 | golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 419 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 420 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 421 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 422 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 423 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 424 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 425 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 426 | golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 427 | golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= 428 | golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= 429 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 430 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 431 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 432 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 433 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 434 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 435 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 436 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 437 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 438 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 439 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 440 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 441 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 442 | golang.org/x/sys v0.0.0-20180622082034-63fc586f45fe/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 443 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 444 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 445 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 446 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 447 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 448 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 449 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 450 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 451 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 452 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 453 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 454 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 455 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 456 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 457 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 458 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 459 | golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 460 | golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 461 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 462 | golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 463 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 464 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 465 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 466 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 467 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 468 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 469 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 470 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 471 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 472 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 473 | golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 474 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 475 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 476 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 477 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 478 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 479 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 480 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 481 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 482 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 483 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 484 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 485 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 486 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 487 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 488 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 489 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 490 | golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 491 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 492 | golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 493 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 494 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 495 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 496 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 497 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 498 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 499 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 500 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 501 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 502 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 503 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 504 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 505 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 506 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 507 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 508 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 509 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 510 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 511 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 512 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 513 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 514 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 515 | golang.org/x/tools v0.0.0-20191203134012-c197fd4bf371/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 516 | golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 517 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 518 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 519 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 520 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 521 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 522 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 523 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 524 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 525 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 526 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 527 | golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= 528 | golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= 529 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 530 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 531 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 532 | google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= 533 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 534 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 535 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 536 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 537 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 538 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 539 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 540 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 541 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 542 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 543 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 544 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 545 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 546 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 547 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 548 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 549 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 550 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 551 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 552 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 553 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 554 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 555 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 556 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 557 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 558 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 559 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 560 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 561 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 562 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 563 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 564 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 565 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 566 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 567 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 568 | google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 569 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 570 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 571 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 572 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 573 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 574 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 575 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 576 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 577 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 578 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 579 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 580 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 581 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 582 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 583 | gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE= 584 | gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 585 | gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 586 | gopkg.in/ns1/ns1-go.v2 v2.0.0-20190730140822-b51389932cbc/go.mod h1:VV+3haRsgDiVLxyifmMBrBIuCWFBPYKbRssXB9z67Hw= 587 | gopkg.in/resty.v1 v1.9.1/go.mod h1:vo52Hzryw9PnPHcJfPsBiFW62XhNx5OczbV9y+IMpgc= 588 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 589 | gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= 590 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 591 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 592 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 593 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 594 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 595 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 596 | gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 597 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 598 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 599 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 600 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 601 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 602 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 603 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 604 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 605 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 606 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 607 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 608 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 609 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 610 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 611 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 612 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | //go:build !test 2 | // +build !test 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | "crypto/tls" 9 | "flag" 10 | stdlog "log" 11 | "net/http" 12 | "os" 13 | "strings" 14 | "syscall" 15 | 16 | "github.com/caddyserver/certmagic" 17 | legolog "github.com/go-acme/lego/v3/log" 18 | "github.com/julienschmidt/httprouter" 19 | "github.com/rs/cors" 20 | log "github.com/sirupsen/logrus" 21 | ) 22 | 23 | func main() { 24 | // Created files are not world writable 25 | syscall.Umask(0077) 26 | configPtr := flag.String("c", "/etc/acme-dns/config.cfg", "config file location") 27 | flag.Parse() 28 | // Read global config 29 | var err error 30 | if fileIsAccessible(*configPtr) { 31 | log.WithFields(log.Fields{"file": *configPtr}).Info("Using config file") 32 | Config, err = readConfig(*configPtr) 33 | } else if fileIsAccessible("./config.cfg") { 34 | log.WithFields(log.Fields{"file": "./config.cfg"}).Info("Using config file") 35 | Config, err = readConfig("./config.cfg") 36 | } else { 37 | log.Errorf("Configuration file not found.") 38 | os.Exit(1) 39 | } 40 | if err != nil { 41 | log.Errorf("Encountered an error while trying to read configuration file: %s", err) 42 | os.Exit(1) 43 | } 44 | 45 | setupLogging(Config.Logconfig.Format, Config.Logconfig.Level) 46 | 47 | // Open database 48 | newDB := new(acmedb) 49 | err = newDB.Init(Config.Database.Engine, Config.Database.Connection) 50 | if err != nil { 51 | log.Errorf("Could not open database [%v]", err) 52 | os.Exit(1) 53 | } else { 54 | log.Info("Connected to database") 55 | } 56 | DB = newDB 57 | defer DB.Close() 58 | 59 | // Error channel for servers 60 | errChan := make(chan error, 1) 61 | 62 | // DNS server 63 | dnsservers := make([]*DNSServer, 0) 64 | if strings.HasPrefix(Config.General.Proto, "both") { 65 | // Handle the case where DNS server should be started for both udp and tcp 66 | udpProto := "udp" 67 | tcpProto := "tcp" 68 | if strings.HasSuffix(Config.General.Proto, "4") { 69 | udpProto += "4" 70 | tcpProto += "4" 71 | } else if strings.HasSuffix(Config.General.Proto, "6") { 72 | udpProto += "6" 73 | tcpProto += "6" 74 | } 75 | dnsServerUDP := NewDNSServer(DB, Config.General.Listen, udpProto, Config.General.Domain) 76 | dnsservers = append(dnsservers, dnsServerUDP) 77 | dnsServerUDP.ParseRecords(Config) 78 | dnsServerTCP := NewDNSServer(DB, Config.General.Listen, tcpProto, Config.General.Domain) 79 | dnsservers = append(dnsservers, dnsServerTCP) 80 | // No need to parse records from config again 81 | dnsServerTCP.Domains = dnsServerUDP.Domains 82 | dnsServerTCP.SOA = dnsServerUDP.SOA 83 | go dnsServerUDP.Start(errChan) 84 | go dnsServerTCP.Start(errChan) 85 | } else { 86 | dnsServer := NewDNSServer(DB, Config.General.Listen, Config.General.Proto, Config.General.Domain) 87 | dnsservers = append(dnsservers, dnsServer) 88 | dnsServer.ParseRecords(Config) 89 | go dnsServer.Start(errChan) 90 | } 91 | 92 | // HTTP API 93 | go startHTTPAPI(errChan, Config, dnsservers) 94 | 95 | // block waiting for error 96 | for { 97 | err = <-errChan 98 | if err != nil { 99 | log.Fatal(err) 100 | } 101 | } 102 | } 103 | 104 | func startHTTPAPI(errChan chan error, config DNSConfig, dnsservers []*DNSServer) { 105 | // Setup http logger 106 | logger := log.New() 107 | logwriter := logger.Writer() 108 | defer logwriter.Close() 109 | // Setup logging for different dependencies to log with logrus 110 | // Certmagic 111 | stdlog.SetOutput(logwriter) 112 | // Lego 113 | legolog.Logger = logger 114 | 115 | api := httprouter.New() 116 | c := cors.New(cors.Options{ 117 | AllowedOrigins: Config.API.CorsOrigins, 118 | AllowedMethods: []string{"GET", "POST"}, 119 | OptionsPassthrough: false, 120 | Debug: Config.General.Debug, 121 | }) 122 | if Config.General.Debug { 123 | // Logwriter for saner log output 124 | c.Log = stdlog.New(logwriter, "", 0) 125 | } 126 | if !Config.API.DisableRegistration { 127 | api.POST("/register", webRegisterPost) 128 | } 129 | api.POST("/update", Auth(webUpdatePost)) 130 | api.GET("/health", healthCheck) 131 | 132 | host := Config.API.IP + ":" + Config.API.Port 133 | 134 | // TLS specific general settings 135 | cfg := &tls.Config{ 136 | MinVersion: tls.VersionTLS12, 137 | } 138 | provider := NewChallengeProvider(dnsservers) 139 | storage := certmagic.FileStorage{Path: Config.API.ACMECacheDir} 140 | 141 | // Set up certmagic for getting certificate for acme-dns api 142 | certmagic.DefaultACME.DNS01Solver = &provider 143 | certmagic.DefaultACME.Agreed = true 144 | if Config.API.TLS == "letsencrypt" { 145 | certmagic.DefaultACME.CA = certmagic.LetsEncryptProductionCA 146 | } else { 147 | certmagic.DefaultACME.CA = certmagic.LetsEncryptStagingCA 148 | } 149 | certmagic.DefaultACME.Email = Config.API.NotificationEmail 150 | magicConf := certmagic.NewDefault() 151 | magicConf.Storage = &storage 152 | magicConf.DefaultServerName = Config.General.Domain 153 | 154 | magicCache := certmagic.NewCache(certmagic.CacheOptions{ 155 | GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) { 156 | return magicConf, nil 157 | }, 158 | }) 159 | 160 | magic := certmagic.New(magicCache, *magicConf) 161 | var err error 162 | switch Config.API.TLS { 163 | case "letsencryptstaging": 164 | err = magic.ManageAsync(context.Background(), []string{Config.General.Domain}) 165 | if err != nil { 166 | errChan <- err 167 | return 168 | } 169 | cfg.GetCertificate = magic.GetCertificate 170 | 171 | srv := &http.Server{ 172 | Addr: host, 173 | Handler: c.Handler(api), 174 | TLSConfig: cfg, 175 | ErrorLog: stdlog.New(logwriter, "", 0), 176 | } 177 | log.WithFields(log.Fields{"host": host, "domain": Config.General.Domain}).Info("Listening HTTPS") 178 | err = srv.ListenAndServeTLS("", "") 179 | case "letsencrypt": 180 | err = magic.ManageAsync(context.Background(), []string{Config.General.Domain}) 181 | if err != nil { 182 | errChan <- err 183 | return 184 | } 185 | cfg.GetCertificate = magic.GetCertificate 186 | srv := &http.Server{ 187 | Addr: host, 188 | Handler: c.Handler(api), 189 | TLSConfig: cfg, 190 | ErrorLog: stdlog.New(logwriter, "", 0), 191 | } 192 | log.WithFields(log.Fields{"host": host, "domain": Config.General.Domain}).Info("Listening HTTPS") 193 | err = srv.ListenAndServeTLS("", "") 194 | case "cert": 195 | srv := &http.Server{ 196 | Addr: host, 197 | Handler: c.Handler(api), 198 | TLSConfig: cfg, 199 | ErrorLog: stdlog.New(logwriter, "", 0), 200 | } 201 | log.WithFields(log.Fields{"host": host}).Info("Listening HTTPS") 202 | err = srv.ListenAndServeTLS(Config.API.TLSCertFullchain, Config.API.TLSCertPrivkey) 203 | default: 204 | log.WithFields(log.Fields{"host": host}).Info("Listening HTTP") 205 | err = http.ListenAndServe(host, c.Handler(api)) 206 | } 207 | if err != nil { 208 | errChan <- err 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "os" 8 | "sync" 9 | "testing" 10 | 11 | log "github.com/sirupsen/logrus" 12 | logrustest "github.com/sirupsen/logrus/hooks/test" 13 | ) 14 | 15 | var loghook = new(logrustest.Hook) 16 | var dnsserver *DNSServer 17 | 18 | var ( 19 | postgres = flag.Bool("postgres", false, "run integration tests against PostgreSQL") 20 | ) 21 | 22 | var records = []string{ 23 | "auth.example.org. A 192.168.1.100", 24 | "ns1.auth.example.org. A 192.168.1.101", 25 | "cn.example.org CNAME something.example.org.", 26 | "!''b', unparseable ", 27 | "ns2.auth.example.org. A 192.168.1.102", 28 | } 29 | 30 | func TestMain(m *testing.M) { 31 | setupTestLogger() 32 | setupConfig() 33 | flag.Parse() 34 | 35 | newDb := new(acmedb) 36 | if *postgres { 37 | Config.Database.Engine = "postgres" 38 | err := newDb.Init("postgres", "postgres://acmedns:acmedns@localhost/acmedns") 39 | if err != nil { 40 | fmt.Println("PostgreSQL integration tests expect database \"acmedns\" running in localhost, with username and password set to \"acmedns\"") 41 | os.Exit(1) 42 | } 43 | } else { 44 | Config.Database.Engine = "sqlite3" 45 | _ = newDb.Init("sqlite3", ":memory:") 46 | } 47 | DB = newDb 48 | dnsserver = NewDNSServer(DB, Config.General.Listen, Config.General.Proto, Config.General.Domain) 49 | dnsserver.ParseRecords(Config) 50 | 51 | // Make sure that we're not creating a race condition in tests 52 | var wg sync.WaitGroup 53 | wg.Add(1) 54 | dnsserver.Server.NotifyStartedFunc = func() { 55 | wg.Done() 56 | } 57 | go dnsserver.Start(make(chan error, 1)) 58 | wg.Wait() 59 | exitval := m.Run() 60 | _ = dnsserver.Server.Shutdown() 61 | DB.Close() 62 | os.Exit(exitval) 63 | } 64 | 65 | func setupConfig() { 66 | var dbcfg = dbsettings{ 67 | Engine: "sqlite3", 68 | Connection: ":memory:", 69 | } 70 | 71 | var generalcfg = general{ 72 | Domain: "auth.example.org", 73 | Listen: "127.0.0.1:15353", 74 | Proto: "udp", 75 | Nsname: "ns1.auth.example.org", 76 | Nsadmin: "admin.example.org", 77 | StaticRecords: records, 78 | Debug: false, 79 | } 80 | 81 | var httpapicfg = httpapi{ 82 | Domain: "", 83 | Port: "8080", 84 | TLS: "none", 85 | CorsOrigins: []string{"*"}, 86 | UseHeader: false, 87 | HeaderName: "X-Forwarded-For", 88 | } 89 | 90 | var dnscfg = DNSConfig{ 91 | Database: dbcfg, 92 | General: generalcfg, 93 | API: httpapicfg, 94 | } 95 | 96 | Config = dnscfg 97 | } 98 | 99 | func setupTestLogger() { 100 | log.SetOutput(io.Discard) 101 | log.AddHook(loghook) 102 | } 103 | 104 | func loggerHasEntryWithMessage(message string) bool { 105 | for _, v := range loghook.Entries { 106 | if v.Message == message { 107 | return true 108 | } 109 | } 110 | return false 111 | } 112 | -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # go test doesn't play well with noexec /tmp 3 | sudo mkdir /gotmp 4 | sudo mount tmpfs -t tmpfs /gotmp 5 | TMPDIR=/gotmp go test -v -race 6 | sudo umount /gotmp 7 | sudo rm -rf /gotmp 8 | -------------------------------------------------------------------------------- /test/pgsql.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | sudo -u postgres createdb acmedns 3 | sudo -u postgres psql -c "CREATE USER acmedns WITH PASSWORD 'acmedns'" 4 | sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE acmedns TO acmedns" 5 | -------------------------------------------------------------------------------- /test/run_integration.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source /home/vagrant/.profile 3 | rm -rf /home/vagrant/src/acme-dns/* 4 | cp -R /vagrant/* /home/vagrant/src/acme-dns/ 5 | cd /home/vagrant/src/acme-dns/ 6 | go get 7 | go test -postgres 8 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "sync" 6 | 7 | "github.com/google/uuid" 8 | ) 9 | 10 | // Config is global configuration struct 11 | var Config DNSConfig 12 | 13 | // DB is used to access the database functions in acme-dns 14 | var DB database 15 | 16 | // DNSConfig holds the config structure 17 | type DNSConfig struct { 18 | General general 19 | Database dbsettings 20 | API httpapi 21 | Logconfig logconfig 22 | } 23 | 24 | // Config file general section 25 | type general struct { 26 | Listen string 27 | Proto string `toml:"protocol"` 28 | Domain string 29 | Nsname string 30 | Nsadmin string 31 | Debug bool 32 | StaticRecords []string `toml:"records"` 33 | } 34 | 35 | type dbsettings struct { 36 | Engine string 37 | Connection string 38 | } 39 | 40 | // API config 41 | type httpapi struct { 42 | Domain string `toml:"api_domain"` 43 | IP string 44 | DisableRegistration bool `toml:"disable_registration"` 45 | AutocertPort string `toml:"autocert_port"` 46 | Port string `toml:"port"` 47 | TLS string 48 | TLSCertPrivkey string `toml:"tls_cert_privkey"` 49 | TLSCertFullchain string `toml:"tls_cert_fullchain"` 50 | ACMECacheDir string `toml:"acme_cache_dir"` 51 | NotificationEmail string `toml:"notification_email"` 52 | CorsOrigins []string 53 | UseHeader bool `toml:"use_header"` 54 | HeaderName string `toml:"header_name"` 55 | } 56 | 57 | // Logging config 58 | type logconfig struct { 59 | Level string `toml:"loglevel"` 60 | Logtype string `toml:"logtype"` 61 | File string `toml:"logfile"` 62 | Format string `toml:"logformat"` 63 | } 64 | 65 | type acmedb struct { 66 | Mutex sync.Mutex 67 | DB *sql.DB 68 | } 69 | 70 | type database interface { 71 | Init(string, string) error 72 | Register(cidrslice) (ACMETxt, error) 73 | GetByUsername(uuid.UUID) (ACMETxt, error) 74 | GetTXTForDomain(string) ([]string, error) 75 | Update(ACMETxtPost) error 76 | GetBackend() *sql.DB 77 | SetBackend(*sql.DB) 78 | Close() 79 | } 80 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "errors" 6 | "fmt" 7 | "math/big" 8 | "os" 9 | "regexp" 10 | "strings" 11 | 12 | "github.com/BurntSushi/toml" 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | func jsonError(message string) []byte { 17 | return []byte(fmt.Sprintf("{\"error\": \"%s\"}", message)) 18 | } 19 | 20 | func fileIsAccessible(fname string) bool { 21 | _, err := os.Stat(fname) 22 | if err != nil { 23 | return false 24 | } 25 | f, err := os.Open(fname) 26 | if err != nil { 27 | return false 28 | } 29 | f.Close() 30 | return true 31 | } 32 | 33 | func readConfig(fname string) (DNSConfig, error) { 34 | var conf DNSConfig 35 | _, err := toml.DecodeFile(fname, &conf) 36 | if err != nil { 37 | // Return with config file parsing errors from toml package 38 | return conf, err 39 | } 40 | return prepareConfig(conf) 41 | } 42 | 43 | // prepareConfig checks that mandatory values exist, and can be used to set default values in the future 44 | func prepareConfig(conf DNSConfig) (DNSConfig, error) { 45 | if conf.Database.Engine == "" { 46 | return conf, errors.New("missing database configuration option \"engine\"") 47 | } 48 | if conf.Database.Connection == "" { 49 | return conf, errors.New("missing database configuration option \"connection\"") 50 | } 51 | 52 | // Default values for options added to config to keep backwards compatibility with old config 53 | if conf.API.ACMECacheDir == "" { 54 | conf.API.ACMECacheDir = "api-certs" 55 | } 56 | 57 | return conf, nil 58 | } 59 | 60 | func sanitizeString(s string) string { 61 | // URL safe base64 alphabet without padding as defined in ACME 62 | re, _ := regexp.Compile(`[^A-Za-z\-\_0-9]+`) 63 | return re.ReplaceAllString(s, "") 64 | } 65 | 66 | func sanitizeIPv6addr(s string) string { 67 | // Remove brackets from IPv6 addresses, net.ParseCIDR needs this 68 | re, _ := regexp.Compile(`[\[\]]+`) 69 | return re.ReplaceAllString(s, "") 70 | } 71 | 72 | func generatePassword(length int) string { 73 | ret := make([]byte, length) 74 | const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890-_" 75 | alphalen := big.NewInt(int64(len(alphabet))) 76 | for i := 0; i < length; i++ { 77 | c, _ := rand.Int(rand.Reader, alphalen) 78 | r := int(c.Int64()) 79 | ret[i] = alphabet[r] 80 | } 81 | return string(ret) 82 | } 83 | 84 | func sanitizeDomainQuestion(d string) string { 85 | dom := strings.ToLower(d) 86 | firstDot := strings.Index(d, ".") 87 | if firstDot > 0 { 88 | dom = dom[0:firstDot] 89 | } 90 | return dom 91 | } 92 | 93 | func setupLogging(format string, level string) { 94 | if format == "json" { 95 | log.SetFormatter(&log.JSONFormatter{}) 96 | } 97 | switch level { 98 | default: 99 | log.SetLevel(log.WarnLevel) 100 | case "debug": 101 | log.SetLevel(log.DebugLevel) 102 | case "info": 103 | log.SetLevel(log.InfoLevel) 104 | case "error": 105 | log.SetLevel(log.ErrorLevel) 106 | } 107 | // TODO: file logging 108 | } 109 | 110 | func getIPListFromHeader(header string) []string { 111 | iplist := []string{} 112 | for _, v := range strings.Split(header, ",") { 113 | if len(v) > 0 { 114 | // Ignore empty values 115 | iplist = append(iplist, strings.TrimSpace(v)) 116 | } 117 | } 118 | return iplist 119 | } 120 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "syscall" 6 | "testing" 7 | 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | func TestSetupLogging(t *testing.T) { 12 | for i, test := range []struct { 13 | format string 14 | level string 15 | expected string 16 | }{ 17 | {"text", "warning", "warning"}, 18 | {"json", "debug", "debug"}, 19 | {"text", "info", "info"}, 20 | {"json", "error", "error"}, 21 | {"text", "something", "warning"}, 22 | } { 23 | setupLogging(test.format, test.level) 24 | if log.GetLevel().String() != test.expected { 25 | t.Errorf("Test %d: Expected loglevel %s but got %s", i, test.expected, log.GetLevel().String()) 26 | } 27 | } 28 | } 29 | 30 | func TestReadConfig(t *testing.T) { 31 | for i, test := range []struct { 32 | inFile []byte 33 | output DNSConfig 34 | }{ 35 | { 36 | []byte("[general]\nlisten = \":53\"\ndebug = true\n[api]\napi_domain = \"something.strange\""), 37 | DNSConfig{ 38 | General: general{ 39 | Listen: ":53", 40 | Debug: true, 41 | }, 42 | API: httpapi{ 43 | Domain: "something.strange", 44 | }, 45 | }, 46 | }, 47 | 48 | { 49 | []byte("[\x00[[[[[[[[[de\nlisten =]"), 50 | DNSConfig{}, 51 | }, 52 | } { 53 | tmpfile, err := os.CreateTemp("", "acmedns") 54 | if err != nil { 55 | t.Error("Could not create temporary file") 56 | } 57 | defer os.Remove(tmpfile.Name()) 58 | 59 | if _, err := tmpfile.Write(test.inFile); err != nil { 60 | t.Error("Could not write to temporary file") 61 | } 62 | 63 | if err := tmpfile.Close(); err != nil { 64 | t.Error("Could not close temporary file") 65 | } 66 | ret, _ := readConfig(tmpfile.Name()) 67 | if ret.General.Listen != test.output.General.Listen { 68 | t.Errorf("Test %d: Expected listen value %s, but got %s", i, test.output.General.Listen, ret.General.Listen) 69 | } 70 | if ret.API.Domain != test.output.API.Domain { 71 | t.Errorf("Test %d: Expected HTTP API domain %s, but got %s", i, test.output.API.Domain, ret.API.Domain) 72 | } 73 | } 74 | } 75 | 76 | func TestGetIPListFromHeader(t *testing.T) { 77 | for i, test := range []struct { 78 | input string 79 | output []string 80 | }{ 81 | {"1.1.1.1, 2.2.2.2", []string{"1.1.1.1", "2.2.2.2"}}, 82 | {" 1.1.1.1 , 2.2.2.2", []string{"1.1.1.1", "2.2.2.2"}}, 83 | {",1.1.1.1 ,2.2.2.2", []string{"1.1.1.1", "2.2.2.2"}}, 84 | } { 85 | res := getIPListFromHeader(test.input) 86 | if len(res) != len(test.output) { 87 | t.Errorf("Test %d: Expected [%d] items in return list, but got [%d]", i, len(test.output), len(res)) 88 | } else { 89 | 90 | for j, vv := range test.output { 91 | if res[j] != vv { 92 | t.Errorf("Test %d: Expected return value [%v] but got [%v]", j, test.output, res) 93 | } 94 | 95 | } 96 | } 97 | } 98 | } 99 | 100 | func TestFileCheckPermissionDenied(t *testing.T) { 101 | tmpfile, err := os.CreateTemp("", "acmedns") 102 | if err != nil { 103 | t.Error("Could not create temporary file") 104 | } 105 | defer os.Remove(tmpfile.Name()) 106 | _ = syscall.Chmod(tmpfile.Name(), 0000) 107 | if fileIsAccessible(tmpfile.Name()) { 108 | t.Errorf("File should not be accessible") 109 | } 110 | _ = syscall.Chmod(tmpfile.Name(), 0644) 111 | } 112 | 113 | func TestFileCheckNotExists(t *testing.T) { 114 | if fileIsAccessible("/path/that/does/not/exist") { 115 | t.Errorf("File should not be accessible") 116 | } 117 | } 118 | 119 | func TestFileCheckOK(t *testing.T) { 120 | tmpfile, err := os.CreateTemp("", "acmedns") 121 | if err != nil { 122 | t.Error("Could not create temporary file") 123 | } 124 | defer os.Remove(tmpfile.Name()) 125 | if !fileIsAccessible(tmpfile.Name()) { 126 | t.Errorf("File should be accessible") 127 | } 128 | } 129 | 130 | func TestPrepareConfig(t *testing.T) { 131 | for i, test := range []struct { 132 | input DNSConfig 133 | shoulderror bool 134 | }{ 135 | {DNSConfig{Database: dbsettings{Engine: "whatever", Connection: "whatever_too"}}, false}, 136 | {DNSConfig{Database: dbsettings{Engine: "", Connection: "whatever_too"}}, true}, 137 | {DNSConfig{Database: dbsettings{Engine: "whatever", Connection: ""}}, true}, 138 | } { 139 | _, err := prepareConfig(test.input) 140 | if test.shoulderror { 141 | if err == nil { 142 | t.Errorf("Test %d: Expected error with prepareConfig input data [%v]", i, test.input) 143 | } 144 | } else { 145 | if err != nil { 146 | t.Errorf("Test %d: Expected no error with prepareConfig input data [%v]", i, test.input) 147 | } 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /validation.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "unicode/utf8" 5 | "regexp" 6 | 7 | "github.com/google/uuid" 8 | "golang.org/x/crypto/bcrypt" 9 | ) 10 | 11 | func getValidUsername(u string) (uuid.UUID, error) { 12 | uname, err := uuid.Parse(u) 13 | if err != nil { 14 | return uuid.UUID{}, err 15 | } 16 | return uname, nil 17 | } 18 | 19 | func validKey(k string) bool { 20 | kn := sanitizeString(k) 21 | if utf8.RuneCountInString(k) == 40 && utf8.RuneCountInString(kn) == 40 { 22 | // Correct length and all chars valid 23 | return true 24 | } 25 | return false 26 | } 27 | 28 | func validSubdomain(s string) bool { 29 | // URL safe base64 alphabet without padding as defined in ACME 30 | RegExp := regexp.MustCompile("^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$") 31 | return RegExp.MatchString(s) 32 | } 33 | 34 | func validTXT(s string) bool { 35 | sn := sanitizeString(s) 36 | if utf8.RuneCountInString(s) == 43 && utf8.RuneCountInString(sn) == 43 { 37 | // 43 chars is the current LE auth key size, but not limited / defined by ACME 38 | return true 39 | } 40 | return false 41 | } 42 | 43 | func correctPassword(pw string, hash string) bool { 44 | if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(pw)); err == nil { 45 | return true 46 | } 47 | return false 48 | } 49 | -------------------------------------------------------------------------------- /validation_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | func TestGetValidUsername(t *testing.T) { 10 | v1, _ := uuid.Parse("a097455b-52cc-4569-90c8-7a4b97c6eba8") 11 | for i, test := range []struct { 12 | uname string 13 | output uuid.UUID 14 | shouldErr bool 15 | }{ 16 | {"a097455b-52cc-4569-90c8-7a4b97c6eba8", v1, false}, 17 | {"a-97455b-52cc-4569-90c8-7a4b97c6eba8", uuid.UUID{}, true}, 18 | {"", uuid.UUID{}, true}, 19 | {"&!#!25123!%!'%", uuid.UUID{}, true}, 20 | } { 21 | ret, err := getValidUsername(test.uname) 22 | if test.shouldErr && err == nil { 23 | t.Errorf("Test %d: Expected error, but there was none", i) 24 | } 25 | if !test.shouldErr && err != nil { 26 | t.Errorf("Test %d: Expected no error, but got [%v]", i, err) 27 | } 28 | if ret != test.output { 29 | t.Errorf("Test %d: Expected return value %v, but got %v", i, test.output, ret) 30 | } 31 | } 32 | } 33 | 34 | func TestValidKey(t *testing.T) { 35 | for i, test := range []struct { 36 | key string 37 | output bool 38 | }{ 39 | {"", false}, 40 | {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true}, 41 | {"aaaaaaaa-aaa-aaaaaa-aaaaaaaa-aaa_aacaaaa", true}, 42 | {"aaaaaaaa-aaa-aaaaaa#aaaaaaaa-aaa_aacaaaa", false}, 43 | {"aaaaaaaa-aaa-aaaaaa-aaaaaaaa-aaa_aacaaaaa", false}, 44 | } { 45 | ret := validKey(test.key) 46 | if ret != test.output { 47 | t.Errorf("Test %d: Expected return value %t, but got %t", i, test.output, ret) 48 | } 49 | } 50 | } 51 | 52 | func TestGetValidSubdomain(t *testing.T) { 53 | for i, test := range []struct { 54 | subdomain string 55 | output bool 56 | }{ 57 | {"a097455b-52cc-4569-90c8-7a4b97c6eba8", true}, 58 | {"a-97455b-52cc-4569-90c8-7a4b97c6eba8", true}, 59 | {"foo.example.com", false}, 60 | {"foo-example-com", true}, 61 | {"", false}, 62 | {"&!#!25123!%!'%", false}, 63 | } { 64 | ret := validSubdomain(test.subdomain) 65 | if ret != test.output { 66 | t.Errorf("Test %d: Expected return value %t, but got %t", i, test.output, ret) 67 | } 68 | } 69 | } 70 | 71 | func TestValidTXT(t *testing.T) { 72 | for i, test := range []struct { 73 | txt string 74 | output bool 75 | }{ 76 | {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true}, 77 | {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", false}, 78 | {"aaaaaaaaaaaaaaaaaaaaaaaaaaaa#aaaaaaaaaaaaaa", false}, 79 | {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", false}, 80 | {"", false}, 81 | } { 82 | ret := validTXT(test.txt) 83 | if ret != test.output { 84 | t.Errorf("Test %d: Expected return value %t, but got %t", i, test.output, ret) 85 | } 86 | } 87 | } 88 | 89 | func TestCorrectPassword(t *testing.T) { 90 | for i, test := range []struct { 91 | pw string 92 | hash string 93 | output bool 94 | }{ 95 | {"PUrNTjU24JYNEOCeS2JcjaJGv1sinT80oV9--dpX", 96 | "$2a$10$ldVoGU5yrdlbPzuPUbUfleVovGjaRelP9tql0IltVUJk778gf.2tu", 97 | true}, 98 | {"PUrNTjU24JYNEOCeS2JcjaJGv1sinT80oV9--dpX", 99 | "$2a$10$ldVoGU5yrdlbPzuPUbUfleVovGjaRelP9tql0IltVUJk778gf.2t", 100 | false}, 101 | {"PUrNTjU24JYNEOCeS2JcjaJGv1sinT80oV9--dp", 102 | "$2a$10$ldVoGU5yrdlbPzuPUbUfleVovGjaRelP9tql0IltVUJk778gf.2tu", 103 | false}, 104 | {"", "", false}, 105 | } { 106 | ret := correctPassword(test.pw, test.hash) 107 | if ret != test.output { 108 | t.Errorf("Test %d: Expected return value %t, but got %t", i, test.output, ret) 109 | } 110 | } 111 | } 112 | 113 | func TestGetValidCIDRMasks(t *testing.T) { 114 | for i, test := range []struct { 115 | input cidrslice 116 | output cidrslice 117 | }{ 118 | {cidrslice{"10.0.0.1/24"}, cidrslice{"10.0.0.1/24"}}, 119 | {cidrslice{"invalid", "127.0.0.1/32"}, cidrslice{"127.0.0.1/32"}}, 120 | {cidrslice{"2002:c0a8::0/32", "8.8.8.8/32"}, cidrslice{"2002:c0a8::0/32", "8.8.8.8/32"}}, 121 | } { 122 | ret := test.input.ValidEntries() 123 | if len(ret) == len(test.output) { 124 | for i, v := range ret { 125 | if v != test.output[i] { 126 | t.Errorf("Test %d: Expected %q but got %q", i, test.output, ret) 127 | } 128 | } 129 | } else { 130 | t.Errorf("Test %d: Expected %q but got %q", i, test.output, ret) 131 | } 132 | } 133 | } 134 | --------------------------------------------------------------------------------