Compare commits

..

No commits in common. "master" and "0.1.0" have entirely different histories.

1373 changed files with 58803 additions and 222521 deletions

View File

@ -1,70 +1,89 @@
--- ---
kind: pipeline kind: pipeline
type: docker type: docker
name: build-linux name: cleanup-before
environment:
GOOS: linux
GOOPTIONS: -mod=vendor
SRCFILES: cmd/pki/*.go
PROJECTNAME: pki
steps: steps:
- name: build-linux-amd64 - name: clean
image: golang image: alpine
commands: commands:
- go build -o $PROJECTNAME $GOOPTIONS $SRCFILES - rm -rf /build/*
environment: volumes:
GOARCH: amd64 - name: build
path: /build
when: when:
event: event: tag
exclude:
- tag volumes:
- name: build-linux-arm64 - name: build
image: golang host:
commands: path: /tmp/pki/build
- go build -o $PROJECTNAME $GOOPTIONS $SRCFILES
environment:
GOARCH: arm64
when:
event:
exclude:
- tag
--- ---
kind: pipeline kind: pipeline
type: docker type: docker
name: gitea-release-linux name: default-linux-amd64
environment:
GOOS: linux
GOOPTIONS: -mod=vendor
SRCFILES: cmd/pki/*.go
PROJECTNAME: pki
steps: steps:
- name: build-linux-amd64 - name: build
image: golang image: golang
commands: commands:
- go build -o $PROJECTNAME $GOOPTIONS $SRCFILES - ./ci-build.sh build
- tar -czvf $PROJECTNAME-$DRONE_TAG-$GOOS-$GOARCH.tar.gz $PROJECTNAME
- echo $PROJECTNAME $DRONE_TAG > VERSION
environment: environment:
GOOS: linux
GOARCH: amd64 GOARCH: amd64
when: volumes:
event: - name: build
- tag path: /build
- name: build-linux-arm64
volumes:
- name: build
host:
path: /tmp/pki/build
depends_on:
- cleanup-before
---
kind: pipeline
type: docker
name: default-linux-arm64
steps:
- name: build
image: golang image: golang
commands: commands:
- go build -o $PROJECTNAME $GOOPTIONS $SRCFILES - ./ci-build.sh build
- tar -czvf $PROJECTNAME-$DRONE_TAG-$GOOS-$GOARCH.tar.gz $PROJECTNAME
- echo $PROJECTNAME $DRONE_TAG > VERSION
environment: environment:
GOOS: linux
GOARCH: arm64 GOARCH: arm64
volumes:
- name: build
path: /build
volumes:
- name: build
host:
path: /tmp/pki/build
depends_on:
- cleanup-before
---
kind: pipeline
type: docker
name: gitea-release
steps:
- name: move
image: alpine
commands:
- mv build/* ./
volumes:
- name: build
path: /drone/src/build
when: when:
event: event: tag
- tag
- name: release - name: release
image: plugins/gitea-release image: plugins/gitea-release
settings: settings:
@ -76,6 +95,50 @@ steps:
- sha256 - sha256
- sha512 - sha512
title: VERSION title: VERSION
volumes:
- name: build
path: /drone/src/build
when: when:
event: event: tag
- tag - name: ls
image: alpine
commands:
- find .
volumes:
- name: build
path: /drone/src/build
when:
event: tag
volumes:
- name: build
host:
path: /tmp/pki/build
depends_on:
- default-linux-amd64
- default-linux-arm64
---
kind: pipeline
type: docker
name: cleanup-after
steps:
- name: clean
image: alpine
commands:
- rm -rf /build/*
volumes:
- name: build
path: /build
when:
event: tag
volumes:
- name: build
host:
path: /tmp/pki/build
depends_on:
- gitea-release

18
Makefile Normal file
View File

@ -0,0 +1,18 @@
# pki Makefile
GOCMD=go
GOBUILDCMD=${GOCMD} build
GOOPTIONS=-mod=vendor -ldflags="-s -w"
RMCMD=rm
BINNAME=pki
SRCFILES=cmd/pki/*.go
all: build
build:
${GOBUILDCMD} ${GOOPTIONS} ${SRCFILES}
clean:
${RMCMD} -f ${BINNAME}

View File

@ -10,7 +10,7 @@ PKI is a centralized Letsencrypt database server and renewer for certificate man
### Build ### Build
```bash ```bash
go build cmd/pki/pki.go make
``` ```
### Sample config in pki.ini ### Sample config in pki.ini
@ -40,7 +40,7 @@ ovhck=
## License ## License
```text ```text
Copyright (c) 2020, 2021, 2022 PaulBSD Copyright (c) 2020 PaulBSD
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without

60
ci-build.sh Executable file
View File

@ -0,0 +1,60 @@
#!/bin/bash
PROJECTNAME=pki
RELEASENAME=${PROJECTNAME}
VERSION="0"
GOOPTIONS="-mod=vendor"
SRCFILES=cmd/pki/*.go
build() {
echo "Begin of build"
if [[ ! -z $DRONE_TAG ]]
then
echo "Drone tag set, let's do a release"
VERSION=$DRONE_TAG
echo "${PROJECTNAME} ${VERSION}" > /build/VERSION
elif [[ ! -z $DRONE_TAG ]]
then
echo "Drone not set, let's only do a build"
VERSION=$DRONE_COMMIT
fi
if [[ ! -z $VERSION && ! -z $GOOS && ! -z $GOARCH ]]
then
echo "Let's set a release name"
RELEASENAME=${PROJECTNAME}-${VERSION}-${GOOS}-${GOARCH}
fi
echo "Building project"
go build -o ${PROJECTNAME} ${GOOPTIONS} ${SRCFILES}
if [[ ! -z $DRONE_TAG ]]
then
echo "Let's make archives"
mkdir -p /build
tar -czvf /build/${RELEASENAME}.tar.gz ${PROJECTNAME}
fi
echo "Removing binary file"
rm ${PROJECTNAME}
echo "End of build"
}
clean() {
rm -rf $RELEASEDIR
}
case $1 in
"build")
build
;;
"clean")
clean
;;
*)
echo "No options choosen"
exit 1
;;
esac

43
go.mod
View File

@ -1,42 +1,11 @@
module git.paulbsd.com/paulbsd/pki module git.paulbsd.com/paulbsd/pki
go 1.23 go 1.15
require ( require (
github.com/go-acme/lego/v4 v4.17.4 github.com/go-acme/lego/v4 v4.1.0
github.com/golang/snappy v0.0.4 // indirect github.com/labstack/echo/v4 v4.1.17
github.com/labstack/echo/v4 v4.12.0 github.com/lib/pq v1.8.0
github.com/lib/pq v1.10.9 gopkg.in/ini.v1 v1.62.0
github.com/miekg/dns v1.1.62 // indirect xorm.io/xorm v1.0.5
github.com/onsi/ginkgo v1.16.0 // indirect
github.com/onsi/gomega v1.11.0 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/text v0.17.0 // indirect
golang.org/x/time v0.6.0 // indirect
gopkg.in/ini.v1 v1.67.0
xorm.io/builder v0.3.13 // indirect
xorm.io/xorm v1.3.9
)
require (
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/ovh/go-ovh v1.6.0 // indirect
github.com/syndtr/goleveldb v1.0.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/oauth2 v0.22.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/tools v0.24.0 // indirect
) )

678
go.sum
View File

@ -1,228 +1,558 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
contrib.go.opencensus.io/exporter/ocagent v0.4.12/go.mod h1:450APlNTSR6FrvC3CTRqYosuDstRB9un7SOx2k/9ckA=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/Azure/azure-sdk-for-go v32.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/Azure/go-autorest/autorest v0.1.0/go.mod h1:AKyIcETwSUFxIcs/Wnq/C+kwCtlEYGUVd7FPNb2slmg=
github.com/Azure/go-autorest/autorest v0.5.0/go.mod h1:9HLKlQjVBH6U3oDfsXOeVc56THsLPw1L03yban4xThw=
github.com/Azure/go-autorest/autorest/adal v0.1.0/go.mod h1:MeS4XhScH55IST095THyTxElntu7WqB7pNbZo8Q5G3E=
github.com/Azure/go-autorest/autorest/adal v0.2.0/go.mod h1:MeS4XhScH55IST095THyTxElntu7WqB7pNbZo8Q5G3E=
github.com/Azure/go-autorest/autorest/azure/auth v0.1.0/go.mod h1:Gf7/i2FUpyb/sGBLIFxTBzrNzBo7aPXXE3ZVeDRwdpM=
github.com/Azure/go-autorest/autorest/azure/cli v0.1.0/go.mod h1:Dk8CUAt/b/PzkfeRsWzVG9Yj3ps8mS8ECztu43rdU8U=
github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
github.com/Azure/go-autorest/autorest/to v0.2.0/go.mod h1:GunWKJp1AEqgMaGLV+iocmRAJWqST1wQYhyyjXJ3SJc=
github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQvokg3NZAlQTalVMtOIAs1aGK7G6u8=
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
github.com/Azure/go-autorest/tracing v0.1.0/go.mod h1:ROEEAFwXycQw7Sn3DXNtEedEvdeRAgDr0izn4z5Ij88=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87/go.mod h1:iGLljf5n9GjT6kc0HBvyI1nOKnGQbNB66VzSNbK5iks=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/akamai/AkamaiOPEN-edgegrid-golang v0.9.18/go.mod h1:L+HB2uBoDgi3+r1pJEJcbGwyyHhd2QXaGsKLbDwtm8Q=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/aliyun/alibaba-cloud-sdk-go v1.61.458/go.mod h1:pUKYbK5JQ+1Dfxk80P0qxGqe5dkxDoabbZS7zOcouyA=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/aws/aws-sdk-go v1.30.20/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/cenkalti/backoff/v4 v4.0.2 h1:JIufpQLbh4DkbQoii76ItQIUFzevQSqOLZca4eamEDs=
github.com/cenkalti/backoff/v4 v4.0.2/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg=
github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/cloudflare-go v0.13.2/go.mod h1:27kfc1apuifUmJhp069y0+hwlKDg4bd8LWlu7oKeZvM=
github.com/cpu/goacmedns v0.0.3/go.mod h1:4MipLkI+qScwqtVxcNO6okBhbgRrr7/tKXUSgSL0teQ=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
github.com/dnsimple/dnsimple-go v0.63.0/go.mod h1:O5TJ0/U6r7AfT8niYNlmohpLbCSG+c71tQlGr9SeGrg=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/exoscale/egoscale v0.23.0/go.mod h1:hRo78jkjkCDKpivQdRBEpNYF5+cVpCJCPDg2/r45KaY=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/go-acme/lego/v4 v4.1.0 h1:/9pMjaeaLq6m0n+io+kv2ySs2ZfrmH6eazuMoN18GHo=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-acme/lego/v4 v4.1.0/go.mod h1:pIFm5tWkXSgiAEfJ/XQCQIvX1cEvHFwbgLZyx8OVSUE=
github.com/go-acme/lego/v4 v4.16.1 h1:JxZ93s4KG0jL27rZ30UsIgxap6VGzKuREsSkkyzeoCQ= github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s=
github.com/go-acme/lego/v4 v4.16.1/go.mod h1:AVvwdPned/IWpD/ihHhMsKnveF7HHYAz/CmtXi7OZoE= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-acme/lego/v4 v4.17.4 h1:h0nePd3ObP6o7kAkndtpTzCw8shOZuWckNYeUQwo36Q= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-acme/lego/v4 v4.17.4/go.mod h1:dU94SvPNqimEeb7EVilGGSnS0nU1O5Exir0pQ4QFL4U= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.3.4 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gophercloud/gophercloud v0.6.1-0.20191122030953-d8ac278c1c9d/go.mod h1:ozGNgr9KYOVATV5jsgHl/ceCDXGuguqOZAzoQ/2vcNM=
github.com/gophercloud/gophercloud v0.7.0/go.mod h1:gmC5oQqMDOMO1t1gq5DquX/yAU808e/4mzjjDA76+Ss=
github.com/gophercloud/utils v0.0.0-20200508015959-b0167b94122c/go.mod h1:ehWUbLQJPqS0Ep+CxeD559hsm9pthPXadJNKwZkp43w=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-retryablehttp v0.6.7/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhKWFeDesPjMj+wCHReeknARU3wqlyN4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk= github.com/labbsr0x/bindman-dns-webhook v1.0.2/go.mod h1:p6b+VCXIR8NYKpDr8/dg1HKfQoRHCdcsROXKvmoehKA=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= github.com/labbsr0x/goh v1.0.1/go.mod h1:8K2UhVoaWXcCU7Lxoa2omWnC8gyW8px7/lmO61c027w=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= github.com/labstack/echo v1.4.4 h1:1bEiBNeGSUKxcPDGfZ/7IgdhJJZx8wV/pICJh4W2NJI=
github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg=
github.com/labstack/echo/v4 v4.1.17 h1:PQIBaRplyRy3OjwILGkPg89JRtH2x5bssi59G2EL3fo=
github.com/labstack/echo/v4 v4.1.17/go.mod h1:Tn2yRQL/UclUalpb5rPdXDevbkJ+lp/2svdyFBg6CHQ=
github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg=
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/linode/linodego v0.21.0/go.mod h1:UTpq1JUZD0CZsJ8rt+0CRkqbzrp1MbGakVPt2DXY5Mk=
github.com/liquidweb/liquidweb-go v1.6.1/go.mod h1:UDcVnAMDkZxpw4Y7NOHkqoeiGacVLEIG/i5J9cyixzQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/mattn/go-tty v0.0.0-20180219170247-931426f7535a/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.1.31 h1:sJFOl9BgwbYAWOGEwr61FU28pqsBNdpRBnhGXtO06Oo=
github.com/miekg/dns v1.1.31/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-vnc v0.0.0-20150629162542-723ed9867aed/go.mod h1:3rdaFaCv4AyBgu5ALFM0+tSuHrBh6v692nyQe3ikrq0=
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/nrdcg/auroradns v1.0.1/go.mod h1:y4pc0i9QXYlFCWrhWrUSIETnZgrf4KuwjDIWmmXo3JI=
github.com/nrdcg/desec v0.5.0/go.mod h1:2ejvMazkav1VdDbv2HeQO7w+Ta1CGHqzQr27ZBYTuEQ=
github.com/nrdcg/dnspod-go v0.4.0/go.mod h1:vZSoFSFeQVm2gWLMkyX61LZ8HI3BaqtHZWgPTGKr6KQ=
github.com/nrdcg/goinwx v0.8.1/go.mod h1:tILVc10gieBp/5PMvbcYeXM6pVQ+c9jxDZnpaR1UW7c=
github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw=
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/ginkgo v1.16.0 h1:NBrNLB37exjJLxXtFOktx6CISBdS1aF8+7MwKlTV8U4=
github.com/onsi/ginkgo v1.16.0/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/oracle/oci-go-sdk v24.2.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888=
github.com/onsi/gomega v1.11.0 h1:+CqWgvj0OZycCaqclBD1pxKHAU+tOkHmQIWvDHq2aug= github.com/ovh/go-ovh v1.1.0 h1:bHXZmw8nTgZin4Nv7JuaLs0KG5x54EQR7migYTd1zrk=
github.com/onsi/gomega v1.11.0/go.mod h1:azGKhqFUon9Vuj0YmTfLSmx0FUwqXYSTl5re8lQLTUg= github.com/ovh/go-ovh v1.1.0/go.mod h1:AxitLZ5HBRPyUd+Zl60Ajaag+rNTdVXWIkzfrVuTXWA=
github.com/ovh/go-ovh v1.5.1 h1:P8O+7H+NQuFK9P/j4sFW5C0fvSS2DnHYGPwdVCp45wI= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/ovh/go-ovh v1.5.1/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/ovh/go-ovh v1.6.0 h1:ixLOwxQdzYDx296sXcgS35TOPEahJkpjMGtzPadCjQI= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/ovh/go-ovh v1.6.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= github.com/pquerna/otp v1.2.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2/go.mod h1:7tZKcyumwBO6qip7RNQ5r77yrssm9bfCowcLEBcU5IA=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sacloud/libsacloud v1.36.2/go.mod h1:P7YAOVmnIn3DKHqCZcUKYUXmSwGBm3yS7IBEjKVSrjg=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/transip/gotransip/v6 v6.2.0/go.mod h1:pQZ36hWWRahCUXkFWlx9Hs711gLd8J4qdgLdRzmtY+g=
github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g=
github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/vultr/govultr v0.5.0/go.mod h1:wZZXZbYbqyY1n3AldoeYNZK4Wnmmoq6dNFkvd5TV3ss=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/ratelimit v0.0.0-20180316092928-c15da0234277/go.mod h1:2X8KaoNd1J0lZV+PxJk/5+DGbO/tpwLR1m++a7FnB/Y=
golang.org/x/crypto v0.0.0-20180621125126-a49355c7e3f8/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180622082034-63fc586f45fe/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6 h1:DvY3Zkh7KabQE/kfzMvYvKirSiguP9Q/veMtkYyf0o8=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= golang.org/x/tools v0.0.0-20191203134012-c197fd4bf371/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ns1/ns1-go.v2 v2.4.2/go.mod h1:GMnKY+ZuoJ+lVLL+78uSTjwTz2jMazq6AfGKQOYhsPk=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
modernc.org/libc v1.22.2 h1:4U7v51GyhlWqQmwCHj28Rdq2Yzwk55ovjFrdPjs8Hb0= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= xorm.io/builder v0.3.7 h1:2pETdKRK+2QG4mLX4oODHEhn5Z8j1m8sXa7jfu+/SZI=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= xorm.io/builder v0.3.7/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
modernc.org/memory v1.4.0 h1:crykUfNSnMAXaOJnnxcSzbUGMqkLWjklJKkBK2nwZwk= xorm.io/xorm v1.0.5 h1:LRr5PfOUb4ODPR63YwbowkNDwcolT2LnkwP/TUaMaB0=
modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= xorm.io/xorm v1.0.5/go.mod h1:uF9EtbhODq5kNWxMbnBEj8hRRZnlcNSz2t2N7HW/+A4=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.20.4 h1:J8+m2trkN+KKoE7jglyHYYYiaq5xmz2HoHJIiBlRzbE=
modernc.org/sqlite v1.20.4/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A=
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo=
xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/xorm v1.3.9 h1:TUovzS0ko+IQ1XnNLfs5dqK1cJl1H5uHpWbWqAQ04nU=
xorm.io/xorm v1.3.9/go.mod h1:LsCCffeeYp63ssk0pKumP6l96WZcHix7ChpurcLNuMw=

View File

@ -2,18 +2,15 @@ package cert
import "time" import "time"
func (e *Entry) Z() {
}
// Entry is the main struct for stored certificates // Entry is the main struct for stored certificates
type Entry struct { type Entry struct {
ID int `xorm:"pk autoincr"` ID int `xorm:"pk autoincr"`
Domain string `xorm:"notnull"` Domain string `xorm:"notnull"`
Certificate string `xorm:"text notnull"` Certificate string `xorm:"text notnull"`
PrivateKey string `xorm:"text notnull"` PrivateKey string `xorm:"text notnull"`
AuthURL string `xorm:"notnull"` AuthURL string
ValidityBegin time.Time `xorm:"notnull"` ValidityBegin time.Time
ValidityEnd time.Time `xorm:"notnull"` ValidityEnd time.Time
Created time.Time `xorm:"created notnull"` Created time.Time `xorm:"created notnull"`
Updated time.Time `xorm:"updated notnull"` Updated time.Time `xorm:"updated notnull"`
} }

View File

@ -60,13 +60,10 @@ func (cfg *Config) GetConfig() error {
options["ovhas"] = pkisection.Key("ovhas").MustString("") options["ovhas"] = pkisection.Key("ovhas").MustString("")
options["ovhck"] = pkisection.Key("ovhck").MustString("") options["ovhck"] = pkisection.Key("ovhck").MustString("")
options["pdnsapiurl"] = pkisection.Key("pdnsapiurl").MustString("")
options["pdnsapikey"] = pkisection.Key("pdnsapikey").MustString("")
cfg.ACME.ProviderOptions = options cfg.ACME.ProviderOptions = options
for key, value := range options { for k, v := range options {
if value == "" { if v == "" {
utils.Advice(fmt.Sprintf("Provider parameter %s not set", key)) utils.Advice(fmt.Sprintf("OVH provider parameter %s not set", k))
} }
} }
@ -75,8 +72,6 @@ func (cfg *Config) GetConfig() error {
cfg.ACME.AuthURL = lego.LEDirectoryProduction cfg.ACME.AuthURL = lego.LEDirectoryProduction
case "staging": case "staging":
cfg.ACME.AuthURL = lego.LEDirectoryStaging cfg.ACME.AuthURL = lego.LEDirectoryStaging
default:
cfg.ACME.AuthURL = lego.LEDirectoryStaging
} }
return nil return nil

View File

@ -7,7 +7,6 @@ import (
"git.paulbsd.com/paulbsd/pki/src/cert" "git.paulbsd.com/paulbsd/pki/src/cert"
"git.paulbsd.com/paulbsd/pki/src/config" "git.paulbsd.com/paulbsd/pki/src/config"
"git.paulbsd.com/paulbsd/pki/src/domain"
"git.paulbsd.com/paulbsd/pki/src/pki" "git.paulbsd.com/paulbsd/pki/src/pki"
_ "github.com/lib/pq" _ "github.com/lib/pq"
"xorm.io/xorm" "xorm.io/xorm"
@ -18,7 +17,7 @@ import (
func Init(cfg *config.Config) (err error) { func Init(cfg *config.Config) (err error) {
var databaseEngine = "postgres" var databaseEngine = "postgres"
tables := []interface{}{cert.Entry{}, tables := []interface{}{cert.Entry{},
pki.User{}, domain.Domain{}} pki.User{}}
cfg.Db, err = xorm.NewEngine(databaseEngine, cfg.Db, err = xorm.NewEngine(databaseEngine,
fmt.Sprintf("%s://%s:%s@%s/%s", fmt.Sprintf("%s://%s:%s@%s/%s",

View File

@ -1,12 +0,0 @@
package domain
import "time"
// Domain describes a domain
type Domain struct {
ID int `xorm:"pk autoincr"`
Domain string `xorm:"text notnull unique(domain_provider)"`
Provider string `xorm:"text notnull unique(domain_provider)"`
Created time.Time `xorm:"created notnull"`
Updated time.Time `xorm:"updated notnull"`
}

View File

@ -12,10 +12,8 @@ import (
"git.paulbsd.com/paulbsd/pki/src/cert" "git.paulbsd.com/paulbsd/pki/src/cert"
"git.paulbsd.com/paulbsd/pki/src/config" "git.paulbsd.com/paulbsd/pki/src/config"
"git.paulbsd.com/paulbsd/pki/src/domain"
"github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate" "github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration" "github.com/go-acme/lego/v4/registration"
) )
@ -30,14 +28,10 @@ func (u *User) Init(cfg *config.Config) (err error) {
} }
// GetEntry returns requested acme ressource in database relative to domain // GetEntry returns requested acme ressource in database relative to domain
func (u *User) GetEntry(cfg *config.Config, domain *string) (Entry cert.Entry, err error) { func (u *User) GetEntry(cfg *config.Config, domain string) (Entry cert.Entry, err error) {
has, err := cfg.Db.Where("domain = ?", domain).And( has, err := cfg.Db.Where("domain = ?", domain).Get(&Entry)
"auth_url = ?", cfg.ACME.AuthURL).And(
fmt.Sprintf("validity_end::timestamp-'%d DAY'::INTERVAL >= now()", cfg.ACME.MaxDaysBefore)).Desc(
"id").Get(&Entry)
if !has { if !has {
err = fmt.Errorf("entry doesn't exists") err = fmt.Errorf("Entry doesn't exists")
} }
return return
@ -66,50 +60,21 @@ func (u *User) HandleRegistration(cfg *config.Config, client *lego.Client) (err
} }
// RequestNewCert returns a newly requested certificate to letsencrypt // RequestNewCert returns a newly requested certificate to letsencrypt
func (u *User) RequestNewCert(cfg *config.Config, domainnames *[]string) (certs *certificate.Resource, err error) { func (u *User) RequestNewCert(cfg *config.Config, domain string) (certificates *certificate.Resource, err error) {
legoconfig := lego.NewConfig(u) legoconfig := lego.NewConfig(u)
legoconfig.CADirURL = cfg.ACME.AuthURL legoconfig.CADirURL = cfg.ACME.AuthURL
legoconfig.Certificate.KeyType = certcrypto.RSA2048 legoconfig.Certificate.KeyType = certcrypto.RSA2048
var dom domain.Domain ovhprovider, err := initProvider(cfg)
var has bool
for _, d := range *domainnames {
dom = domain.Domain{Domain: d}
if has, err = cfg.Db.Get(&dom); has {
break
}
if err != nil {
log.Println(err)
}
}
if !has {
err = fmt.Errorf("supplied domain not in allowed domains")
return
}
var provider challenge.Provider
switch dom.Provider {
case "ovh":
provider, err = initOVHProvider(cfg)
case "pdns":
provider, err = initPowerDNSProvider(cfg)
default:
return
}
if err != nil {
log.Println(err)
}
client, err := lego.NewClient(legoconfig) client, err := lego.NewClient(legoconfig)
if err != nil { if err != nil {
log.Println(err) log.Fatal(err)
} }
err = client.Challenge.SetDNS01Provider(provider) err = client.Challenge.SetDNS01Provider(ovhprovider)
if err != nil { if err != nil {
log.Println(err) log.Fatal(err)
} }
// If PKICtx doesn't exists, get existing of fetch registration // If PKICtx doesn't exists, get existing of fetch registration
@ -121,15 +86,14 @@ func (u *User) RequestNewCert(cfg *config.Config, domainnames *[]string) (certs
} }
request := certificate.ObtainRequest{ request := certificate.ObtainRequest{
Domains: *domainnames, Domains: []string{domain},
Bundle: true, Bundle: true,
} }
certs, err = client.Certificate.Obtain(request) certificates, err = client.Certificate.Obtain(request)
if err != nil { if err != nil {
log.Println(err) log.Fatal(err)
} }
return return
} }

View File

@ -1,16 +1,12 @@
package pki package pki
import ( import (
"log"
"net/url"
"git.paulbsd.com/paulbsd/pki/src/config" "git.paulbsd.com/paulbsd/pki/src/config"
"github.com/go-acme/lego/v4/providers/dns/ovh" "github.com/go-acme/lego/v4/providers/dns/ovh"
"github.com/go-acme/lego/v4/providers/dns/pdns"
) )
// initOVHProvider initialize DNS provider configuration // initProvider initialize DNS provider configuration
func initOVHProvider(cfg *config.Config) (ovhprovider *ovh.DNSProvider, err error) { func initProvider(cfg *config.Config) (ovhprovider *ovh.DNSProvider, err error) {
ovhconfig := ovh.NewDefaultConfig() ovhconfig := ovh.NewDefaultConfig()
ovhconfig.APIEndpoint = cfg.ACME.ProviderOptions["ovhendpoint"] ovhconfig.APIEndpoint = cfg.ACME.ProviderOptions["ovhendpoint"]
@ -19,24 +15,6 @@ func initOVHProvider(cfg *config.Config) (ovhprovider *ovh.DNSProvider, err erro
ovhconfig.ConsumerKey = cfg.ACME.ProviderOptions["ovhck"] ovhconfig.ConsumerKey = cfg.ACME.ProviderOptions["ovhck"]
ovhprovider, err = ovh.NewDNSProviderConfig(ovhconfig) ovhprovider, err = ovh.NewDNSProviderConfig(ovhconfig)
if err != nil {
log.Println(err)
}
return
}
// initPowerDNSProvider initialize DNS provider configuration
func initPowerDNSProvider(cfg *config.Config) (pdnsprovider *pdns.DNSProvider, err error) {
pdnsconfig := pdns.NewDefaultConfig()
pdnsconfig.Host, err = url.Parse(cfg.ACME.ProviderOptions["pdnsapiurl"])
pdnsconfig.APIKey = cfg.ACME.ProviderOptions["pdnsapikey"]
pdnsprovider, err = pdns.NewDNSProviderConfig(pdnsconfig)
if err != nil {
log.Println(err)
}
return return
} }

View File

@ -27,30 +27,25 @@ func RunServer(cfg *config.Config) (err error) {
e.HideBanner = cfg.Options.HideBanner e.HideBanner = cfg.Options.HideBanner
e.GET("/", func(c echo.Context) error { e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Welcome to PKI software (https://git.paulbsd.com/paulbsd/pki)") username := c.Get("username")
return c.String(http.StatusOK, fmt.Sprintf("username: %s", username))
}) })
e.POST("/cert", func(c echo.Context) (err error) { e.GET("/domain/:domain", func(c echo.Context) (err error) {
var request = new(EntryRequest) var result EntryResponse
var result = make(map[string]EntryResponse) log.Println(fmt.Sprintf("Providing %s to user %s at %s", c.Param("domain"), c.Get("username"), c.RealIP()))
err = c.Bind(&request) result, err = GetCertificate(cfg, c.Get("user").(*pki.User), c.Param("domain"))
if err != nil { if err != nil {
log.Println(err) return c.String(http.StatusInternalServerError, fmt.Sprintf("%s %s", result, err))
return c.JSON(http.StatusInternalServerError, "error parsing request")
}
log.Printf("Providing %s to user %s at %s\n", request.Domains, c.Get("username"), c.RealIP())
result, err = GetCertificate(cfg, c.Get("user").(*pki.User), &request.Domains)
if err != nil {
return c.String(http.StatusInternalServerError, fmt.Sprintf("%s", err))
} }
return c.JSON(http.StatusOK, result) return c.JSON(http.StatusOK, result)
}) })
e.GET("/config", func(c echo.Context) (err error) {
if ConfigAccess(*cfg, c.RealIP()) {
return c.JSON(http.StatusOK, cfg)
}
return c.String(http.StatusForbidden, "Forbidden")
})
e.Logger.Fatal( e.Logger.Fatal(e.Start(fmt.Sprintf(":%d", cfg.Switchs.Port)))
e.Start(
fmt.Sprintf(":%d",
cfg.Switchs.Port)))
return return
} }

View File

@ -13,24 +13,18 @@ import (
"git.paulbsd.com/paulbsd/pki/src/pki" "git.paulbsd.com/paulbsd/pki/src/pki"
) )
const timeformatstring string = "2006-01-02 15:04:05"
var domainRegex, err = regexp.Compile(`^[a-z0-9\*]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,6}$`)
// GetCertificate get certificate from database if exists, of request it from ACME // GetCertificate get certificate from database if exists, of request it from ACME
func GetCertificate(cfg *config.Config, user *pki.User, domains *[]string) (result map[string]EntryResponse, err error) { func GetCertificate(cfg *config.Config, user *pki.User, domain string) (result EntryResponse, err error) {
err = CheckDomains(domains) err = CheckDomain(domain)
if err != nil { if err != nil {
return result, err return result, err
} }
result = make(map[string]EntryResponse)
firstdomain := (*domains)[0] entry, err := user.GetEntry(cfg, domain)
entry, err := user.GetEntry(cfg, &firstdomain)
if err != nil { if err != nil {
certs, err := user.RequestNewCert(cfg, domains) certs, err := user.RequestNewCert(cfg, domain)
if err != nil { if err != nil {
log.Printf("Error fetching new certificate %s\n", err) log.Println(fmt.Sprintf("Error fetching new certificate %s", err))
return result, err return result, err
} }
NotBefore, NotAfter, err := GetDates(certs.Certificate) NotBefore, NotAfter, err := GetDates(certs.Certificate)
@ -38,36 +32,25 @@ func GetCertificate(cfg *config.Config, user *pki.User, domains *[]string) (resu
log.Println("Error where parsing dates") log.Println("Error where parsing dates")
return result, err return result, err
} }
entry := cert.Entry{Domain: certs.Domain, entry := cert.Entry{Domain: domain,
Certificate: string(certs.Certificate), Certificate: string(certs.Certificate),
PrivateKey: string(certs.PrivateKey), PrivateKey: string(certs.PrivateKey),
ValidityBegin: NotBefore, ValidityBegin: NotBefore,
ValidityEnd: NotAfter, ValidityEnd: NotAfter,
AuthURL: cfg.ACME.AuthURL} AuthURL: cfg.ACME.AuthURL}
cfg.Db.Insert(&entry) cfg.Db.Insert(entry)
result[firstdomain] = convertEntryToResponse(entry) result = convertEntryToResponse(entry)
return result, err return result, err
} }
result[firstdomain] = convertEntryToResponse(entry) result = convertEntryToResponse(entry)
return return
} }
// CheckDomains check if requested domains are valid // CheckDomain check if requested domain is valid
func CheckDomains(domains *[]string) (err error) { func CheckDomain(domain string) (err error) {
for _, domain := range *domains { res, err := regexp.Match(`^[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,6}$`, []byte(domain))
err = CheckDomain(&domain)
if err != nil {
return
}
}
return
}
// CheckDomain check if requested domain are valid
func CheckDomain(domain *string) (err error) {
res := domainRegex.Match([]byte(*domain))
if !res { if !res {
return fmt.Errorf("Domain %s has not a valid syntax %s, please verify", *domain, err) return fmt.Errorf("Domain has not a valid syntax")
} }
return return
} }
@ -78,7 +61,7 @@ func GetDates(cert []byte) (NotBefore time.Time, NotAfter time.Time, err error)
if block.Type == "CERTIFICATE" { if block.Type == "CERTIFICATE" {
ce, err := x509.ParseCertificate(block.Bytes) ce, err := x509.ParseCertificate(block.Bytes)
if err != nil { if err != nil {
log.Println("Error when parsing certificate") log.Fatal("Error when parsing certificate")
} }
NotBefore = ce.NotBefore NotBefore = ce.NotBefore
NotAfter = ce.NotAfter NotAfter = ce.NotAfter
@ -86,27 +69,27 @@ func GetDates(cert []byte) (NotBefore time.Time, NotAfter time.Time, err error)
return return
} }
// NeedRenewal is an unimplemented method
func NeedRenewal(cfg config.Config) (res bool, err error) {
return
}
// convertEntryToResponse converts database ACME entry to JSON ACME entry // convertEntryToResponse converts database ACME entry to JSON ACME entry
func convertEntryToResponse(in cert.Entry) (out EntryResponse) { func convertEntryToResponse(in cert.Entry) (out EntryResponse) {
out.Domains = append(out.Domains, in.Domain) out.Domain = in.Domain
out.Certificate = in.Certificate out.Certificate = in.Certificate
out.PrivateKey = in.PrivateKey out.PrivateKey = in.PrivateKey
out.ValidityBegin = in.ValidityBegin.Format(timeformatstring) out.ValidityBegin = in.ValidityBegin
out.ValidityEnd = in.ValidityEnd.Format(timeformatstring) out.ValidityEnd = in.ValidityEnd
return return
} }
// EntryRequest
type EntryRequest struct {
Domains []string `json:"domains"`
}
// EntryResponse is the struct defining JSON response from webservice // EntryResponse is the struct defining JSON response from webservice
type EntryResponse struct { type EntryResponse struct {
Domains []string `json:"domains"` Domain string `json:"domain"`
Certificate string `json:"certificate"` Certificate string `json:"certificate"`
PrivateKey string `json:"privatekey"` PrivateKey string `json:"privatekey"`
ValidityBegin string `json:"validitybegin"` ValidityBegin time.Time `json:"validitybegin"`
ValidityEnd string `json:"validityend"` ValidityEnd time.Time `json:"validityend"`
} }

View File

@ -13,7 +13,6 @@ import (
// Auth make authentication to webservice // Auth make authentication to webservice
func Auth(cfg *config.Config, username string, password string, c echo.Context) (res bool, user *pki.User, err error) { func Auth(cfg *config.Config, username string, password string, c echo.Context) (res bool, user *pki.User, err error) {
user = &pki.User{Username: username} user = &pki.User{Username: username}
_, err = cfg.Db.Get(user) _, err = cfg.Db.Get(user)
if err != nil { if err != nil {
res = false res = false

View File

@ -20,6 +20,3 @@ _cgo_export.*
_testmain.go _testmain.go
*.exe *.exe
# IDEs
.idea/

10
vendor/github.com/cenkalti/backoff/v4/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,10 @@
language: go
go:
- 1.12
- 1.x
- tip
before_install:
- go get github.com/mattn/goveralls
- go get golang.org/x/tools/cmd/cover
script:
- $HOME/gopath/bin/goveralls -service=travis-ci

View File

@ -1,4 +1,4 @@
# Exponential Backoff [![GoDoc][godoc image]][godoc] [![Coverage Status][coveralls image]][coveralls] # Exponential Backoff [![GoDoc][godoc image]][godoc] [![Build Status][travis image]][travis] [![Coverage Status][coveralls image]][coveralls]
This is a Go port of the exponential backoff algorithm from [Google's HTTP Client Library for Java][google-http-java-client]. This is a Go port of the exponential backoff algorithm from [Google's HTTP Client Library for Java][google-http-java-client].
@ -11,7 +11,8 @@ The retries exponentially increase and stop increasing when a certain threshold
Import path is `github.com/cenkalti/backoff/v4`. Please note the version part at the end. Import path is `github.com/cenkalti/backoff/v4`. Please note the version part at the end.
Use https://pkg.go.dev/github.com/cenkalti/backoff/v4 to view the documentation. godoc.org does not support modules yet,
so you can use https://godoc.org/gopkg.in/cenkalti/backoff.v4 to view the documentation.
## Contributing ## Contributing
@ -19,12 +20,14 @@ Use https://pkg.go.dev/github.com/cenkalti/backoff/v4 to view the documentation.
* Please don't send a PR without opening an issue and discussing it first. * Please don't send a PR without opening an issue and discussing it first.
* If proposed change is not a common use case, I will probably not accept it. * If proposed change is not a common use case, I will probably not accept it.
[godoc]: https://pkg.go.dev/github.com/cenkalti/backoff/v4 [godoc]: https://godoc.org/github.com/cenkalti/backoff
[godoc image]: https://godoc.org/github.com/cenkalti/backoff?status.png [godoc image]: https://godoc.org/github.com/cenkalti/backoff?status.png
[travis]: https://travis-ci.org/cenkalti/backoff
[travis image]: https://travis-ci.org/cenkalti/backoff.png?branch=master
[coveralls]: https://coveralls.io/github/cenkalti/backoff?branch=master [coveralls]: https://coveralls.io/github/cenkalti/backoff?branch=master
[coveralls image]: https://coveralls.io/repos/github/cenkalti/backoff/badge.svg?branch=master [coveralls image]: https://coveralls.io/repos/github/cenkalti/backoff/badge.svg?branch=master
[google-http-java-client]: https://github.com/google/google-http-java-client/blob/da1aa993e90285ec18579f1553339b00e19b3ab5/google-http-client/src/main/java/com/google/api/client/util/ExponentialBackOff.java [google-http-java-client]: https://github.com/google/google-http-java-client/blob/da1aa993e90285ec18579f1553339b00e19b3ab5/google-http-client/src/main/java/com/google/api/client/util/ExponentialBackOff.java
[exponential backoff wiki]: http://en.wikipedia.org/wiki/Exponential_backoff [exponential backoff wiki]: http://en.wikipedia.org/wiki/Exponential_backoff
[advanced example]: https://pkg.go.dev/github.com/cenkalti/backoff/v4?tab=doc#pkg-examples [advanced example]: https://godoc.org/github.com/cenkalti/backoff#example_

View File

@ -57,6 +57,10 @@ func (b *backOffContext) NextBackOff() time.Duration {
case <-b.ctx.Done(): case <-b.ctx.Done():
return Stop return Stop
default: default:
return b.BackOff.NextBackOff()
} }
next := b.BackOff.NextBackOff()
if deadline, ok := b.ctx.Deadline(); ok && deadline.Sub(time.Now()) < next { // nolint: gosimple
return Stop
}
return next
} }

View File

@ -71,9 +71,6 @@ type Clock interface {
Now() time.Time Now() time.Time
} }
// ExponentialBackOffOpts is a function type used to configure ExponentialBackOff options.
type ExponentialBackOffOpts func(*ExponentialBackOff)
// Default values for ExponentialBackOff. // Default values for ExponentialBackOff.
const ( const (
DefaultInitialInterval = 500 * time.Millisecond DefaultInitialInterval = 500 * time.Millisecond
@ -84,7 +81,7 @@ const (
) )
// NewExponentialBackOff creates an instance of ExponentialBackOff using default values. // NewExponentialBackOff creates an instance of ExponentialBackOff using default values.
func NewExponentialBackOff(opts ...ExponentialBackOffOpts) *ExponentialBackOff { func NewExponentialBackOff() *ExponentialBackOff {
b := &ExponentialBackOff{ b := &ExponentialBackOff{
InitialInterval: DefaultInitialInterval, InitialInterval: DefaultInitialInterval,
RandomizationFactor: DefaultRandomizationFactor, RandomizationFactor: DefaultRandomizationFactor,
@ -94,62 +91,10 @@ func NewExponentialBackOff(opts ...ExponentialBackOffOpts) *ExponentialBackOff {
Stop: Stop, Stop: Stop,
Clock: SystemClock, Clock: SystemClock,
} }
for _, fn := range opts {
fn(b)
}
b.Reset() b.Reset()
return b return b
} }
// WithInitialInterval sets the initial interval between retries.
func WithInitialInterval(duration time.Duration) ExponentialBackOffOpts {
return func(ebo *ExponentialBackOff) {
ebo.InitialInterval = duration
}
}
// WithRandomizationFactor sets the randomization factor to add jitter to intervals.
func WithRandomizationFactor(randomizationFactor float64) ExponentialBackOffOpts {
return func(ebo *ExponentialBackOff) {
ebo.RandomizationFactor = randomizationFactor
}
}
// WithMultiplier sets the multiplier for increasing the interval after each retry.
func WithMultiplier(multiplier float64) ExponentialBackOffOpts {
return func(ebo *ExponentialBackOff) {
ebo.Multiplier = multiplier
}
}
// WithMaxInterval sets the maximum interval between retries.
func WithMaxInterval(duration time.Duration) ExponentialBackOffOpts {
return func(ebo *ExponentialBackOff) {
ebo.MaxInterval = duration
}
}
// WithMaxElapsedTime sets the maximum total time for retries.
func WithMaxElapsedTime(duration time.Duration) ExponentialBackOffOpts {
return func(ebo *ExponentialBackOff) {
ebo.MaxElapsedTime = duration
}
}
// WithRetryStopDuration sets the duration after which retries should stop.
func WithRetryStopDuration(duration time.Duration) ExponentialBackOffOpts {
return func(ebo *ExponentialBackOff) {
ebo.Stop = duration
}
}
// WithClockProvider sets the clock used to measure time.
func WithClockProvider(clock Clock) ExponentialBackOffOpts {
return func(ebo *ExponentialBackOff) {
ebo.Clock = clock
}
}
type systemClock struct{} type systemClock struct{}
func (t systemClock) Now() time.Time { func (t systemClock) Now() time.Time {
@ -202,9 +147,6 @@ func (b *ExponentialBackOff) incrementCurrentInterval() {
// Returns a random value from the following interval: // Returns a random value from the following interval:
// [currentInterval - randomizationFactor * currentInterval, currentInterval + randomizationFactor * currentInterval]. // [currentInterval - randomizationFactor * currentInterval, currentInterval + randomizationFactor * currentInterval].
func getRandomValueFromInterval(randomizationFactor, random float64, currentInterval time.Duration) time.Duration { func getRandomValueFromInterval(randomizationFactor, random float64, currentInterval time.Duration) time.Duration {
if randomizationFactor == 0 {
return currentInterval // make sure no randomness is used when randomizationFactor is 0.
}
var delta = randomizationFactor * float64(currentInterval) var delta = randomizationFactor * float64(currentInterval)
var minInterval = float64(currentInterval) - delta var minInterval = float64(currentInterval) - delta
var maxInterval = float64(currentInterval) + delta var maxInterval = float64(currentInterval) + delta

3
vendor/github.com/cenkalti/backoff/v4/go.mod generated vendored Normal file
View File

@ -0,0 +1,3 @@
module github.com/cenkalti/backoff/v4
go 1.12

View File

@ -1,24 +1,11 @@
package backoff package backoff
import ( import "time"
"errors"
"time"
)
// An OperationWithData is executing by RetryWithData() or RetryNotifyWithData().
// The operation will be retried using a backoff policy if it returns an error.
type OperationWithData[T any] func() (T, error)
// An Operation is executing by Retry() or RetryNotify(). // An Operation is executing by Retry() or RetryNotify().
// The operation will be retried using a backoff policy if it returns an error. // The operation will be retried using a backoff policy if it returns an error.
type Operation func() error type Operation func() error
func (o Operation) withEmptyData() OperationWithData[struct{}] {
return func() (struct{}, error) {
return struct{}{}, o()
}
}
// Notify is a notify-on-error function. It receives an operation error and // Notify is a notify-on-error function. It receives an operation error and
// backoff delay if the operation failed (with an error). // backoff delay if the operation failed (with an error).
// //
@ -38,41 +25,18 @@ func Retry(o Operation, b BackOff) error {
return RetryNotify(o, b, nil) return RetryNotify(o, b, nil)
} }
// RetryWithData is like Retry but returns data in the response too.
func RetryWithData[T any](o OperationWithData[T], b BackOff) (T, error) {
return RetryNotifyWithData(o, b, nil)
}
// RetryNotify calls notify function with the error and wait duration // RetryNotify calls notify function with the error and wait duration
// for each failed attempt before sleep. // for each failed attempt before sleep.
func RetryNotify(operation Operation, b BackOff, notify Notify) error { func RetryNotify(operation Operation, b BackOff, notify Notify) error {
return RetryNotifyWithTimer(operation, b, notify, nil) return RetryNotifyWithTimer(operation, b, notify, nil)
} }
// RetryNotifyWithData is like RetryNotify but returns data in the response too.
func RetryNotifyWithData[T any](operation OperationWithData[T], b BackOff, notify Notify) (T, error) {
return doRetryNotify(operation, b, notify, nil)
}
// RetryNotifyWithTimer calls notify function with the error and wait duration using the given Timer // RetryNotifyWithTimer calls notify function with the error and wait duration using the given Timer
// for each failed attempt before sleep. // for each failed attempt before sleep.
// A default timer that uses system timer is used when nil is passed. // A default timer that uses system timer is used when nil is passed.
func RetryNotifyWithTimer(operation Operation, b BackOff, notify Notify, t Timer) error { func RetryNotifyWithTimer(operation Operation, b BackOff, notify Notify, t Timer) error {
_, err := doRetryNotify(operation.withEmptyData(), b, notify, t) var err error
return err var next time.Duration
}
// RetryNotifyWithTimerAndData is like RetryNotifyWithTimer but returns data in the response too.
func RetryNotifyWithTimerAndData[T any](operation OperationWithData[T], b BackOff, notify Notify, t Timer) (T, error) {
return doRetryNotify(operation, b, notify, t)
}
func doRetryNotify[T any](operation OperationWithData[T], b BackOff, notify Notify, t Timer) (T, error) {
var (
err error
next time.Duration
res T
)
if t == nil { if t == nil {
t = &defaultTimer{} t = &defaultTimer{}
} }
@ -85,22 +49,16 @@ func doRetryNotify[T any](operation OperationWithData[T], b BackOff, notify Noti
b.Reset() b.Reset()
for { for {
res, err = operation() if err = operation(); err == nil {
if err == nil { return nil
return res, nil
} }
var permanent *PermanentError if permanent, ok := err.(*PermanentError); ok {
if errors.As(err, &permanent) { return permanent.Err
return res, permanent.Err
} }
if next = b.NextBackOff(); next == Stop { if next = b.NextBackOff(); next == Stop {
if cerr := ctx.Err(); cerr != nil { return err
return res, cerr
}
return res, err
} }
if notify != nil { if notify != nil {
@ -111,7 +69,7 @@ func doRetryNotify[T any](operation OperationWithData[T], b BackOff, notify Noti
select { select {
case <-ctx.Done(): case <-ctx.Done():
return res, ctx.Err() return ctx.Err()
case <-t.C(): case <-t.C():
} }
} }
@ -130,16 +88,8 @@ func (e *PermanentError) Unwrap() error {
return e.Err return e.Err
} }
func (e *PermanentError) Is(target error) bool {
_, ok := target.(*PermanentError)
return ok
}
// Permanent wraps the given err in a *PermanentError. // Permanent wraps the given err in a *PermanentError.
func Permanent(err error) error { func Permanent(err error) *PermanentError {
if err == nil {
return nil
}
return &PermanentError{ return &PermanentError{
Err: err, Err: err,
} }

View File

@ -1,4 +1,4 @@
.DS_Store .DS_Store
bin bin
.idea/

13
vendor/github.com/dgrijalva/jwt-go/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,13 @@
language: go
script:
- go vet ./...
- go test -v ./...
go:
- 1.3
- 1.4
- 1.5
- 1.6
- 1.7
- tip

View File

@ -1,5 +1,4 @@
Copyright (c) 2012 Dave Grijalva Copyright (c) 2012 Dave Grijalva
Copyright (c) 2021 golang-jwt maintainers
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

97
vendor/github.com/dgrijalva/jwt-go/MIGRATION_GUIDE.md generated vendored Normal file
View File

@ -0,0 +1,97 @@
## Migration Guide from v2 -> v3
Version 3 adds several new, frequently requested features. To do so, it introduces a few breaking changes. We've worked to keep these as minimal as possible. This guide explains the breaking changes and how you can quickly update your code.
### `Token.Claims` is now an interface type
The most requested feature from the 2.0 verison of this library was the ability to provide a custom type to the JSON parser for claims. This was implemented by introducing a new interface, `Claims`, to replace `map[string]interface{}`. We also included two concrete implementations of `Claims`: `MapClaims` and `StandardClaims`.
`MapClaims` is an alias for `map[string]interface{}` with built in validation behavior. It is the default claims type when using `Parse`. The usage is unchanged except you must type cast the claims property.
The old example for parsing a token looked like this..
```go
if token, err := jwt.Parse(tokenString, keyLookupFunc); err == nil {
fmt.Printf("Token for user %v expires %v", token.Claims["user"], token.Claims["exp"])
}
```
is now directly mapped to...
```go
if token, err := jwt.Parse(tokenString, keyLookupFunc); err == nil {
claims := token.Claims.(jwt.MapClaims)
fmt.Printf("Token for user %v expires %v", claims["user"], claims["exp"])
}
```
`StandardClaims` is designed to be embedded in your custom type. You can supply a custom claims type with the new `ParseWithClaims` function. Here's an example of using a custom claims type.
```go
type MyCustomClaims struct {
User string
*StandardClaims
}
if token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, keyLookupFunc); err == nil {
claims := token.Claims.(*MyCustomClaims)
fmt.Printf("Token for user %v expires %v", claims.User, claims.StandardClaims.ExpiresAt)
}
```
### `ParseFromRequest` has been moved
To keep this library focused on the tokens without becoming overburdened with complex request processing logic, `ParseFromRequest` and its new companion `ParseFromRequestWithClaims` have been moved to a subpackage, `request`. The method signatues have also been augmented to receive a new argument: `Extractor`.
`Extractors` do the work of picking the token string out of a request. The interface is simple and composable.
This simple parsing example:
```go
if token, err := jwt.ParseFromRequest(tokenString, req, keyLookupFunc); err == nil {
fmt.Printf("Token for user %v expires %v", token.Claims["user"], token.Claims["exp"])
}
```
is directly mapped to:
```go
if token, err := request.ParseFromRequest(req, request.OAuth2Extractor, keyLookupFunc); err == nil {
claims := token.Claims.(jwt.MapClaims)
fmt.Printf("Token for user %v expires %v", claims["user"], claims["exp"])
}
```
There are several concrete `Extractor` types provided for your convenience:
* `HeaderExtractor` will search a list of headers until one contains content.
* `ArgumentExtractor` will search a list of keys in request query and form arguments until one contains content.
* `MultiExtractor` will try a list of `Extractors` in order until one returns content.
* `AuthorizationHeaderExtractor` will look in the `Authorization` header for a `Bearer` token.
* `OAuth2Extractor` searches the places an OAuth2 token would be specified (per the spec): `Authorization` header and `access_token` argument
* `PostExtractionFilter` wraps an `Extractor`, allowing you to process the content before it's parsed. A simple example is stripping the `Bearer ` text from a header
### RSA signing methods no longer accept `[]byte` keys
Due to a [critical vulnerability](https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/), we've decided the convenience of accepting `[]byte` instead of `rsa.PublicKey` or `rsa.PrivateKey` isn't worth the risk of misuse.
To replace this behavior, we've added two helper methods: `ParseRSAPrivateKeyFromPEM(key []byte) (*rsa.PrivateKey, error)` and `ParseRSAPublicKeyFromPEM(key []byte) (*rsa.PublicKey, error)`. These are just simple helpers for unpacking PEM encoded PKCS1 and PKCS8 keys. If your keys are encoded any other way, all you need to do is convert them to the `crypto/rsa` package's types.
```go
func keyLookupFunc(*Token) (interface{}, error) {
// Don't forget to validate the alg is what you expect:
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
// Look up key
key, err := lookupPublicKey(token.Header["kid"])
if err != nil {
return nil, err
}
// Unpack key from PEM encoded PKCS8
return jwt.ParseRSAPublicKeyFromPEM(key)
}
```

View File

@ -1,34 +1,25 @@
# jwt-go # jwt-go
[![build](https://github.com/golang-jwt/jwt/actions/workflows/build.yml/badge.svg)](https://github.com/golang-jwt/jwt/actions/workflows/build.yml) [![Build Status](https://travis-ci.org/dgrijalva/jwt-go.svg?branch=master)](https://travis-ci.org/dgrijalva/jwt-go)
[![Go Reference](https://pkg.go.dev/badge/github.com/golang-jwt/jwt.svg)](https://pkg.go.dev/github.com/golang-jwt/jwt) [![GoDoc](https://godoc.org/github.com/dgrijalva/jwt-go?status.svg)](https://godoc.org/github.com/dgrijalva/jwt-go)
A [go](http://www.golang.org) (or 'golang' for search engine friendliness) implementation of [JSON Web Tokens](https://datatracker.ietf.org/doc/html/rfc7519). A [go](http://www.golang.org) (or 'golang' for search engine friendliness) implementation of [JSON Web Tokens](http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html)
**IMPORT PATH CHANGE:** Starting from [v3.2.1](https://github.com/golang-jwt/jwt/releases/tag/v3.2.1), the import path has changed from `github.com/dgrijalva/jwt-go` to `github.com/golang-jwt/jwt`. After the original author of the library suggested migrating the maintenance of `jwt-go`, a dedicated team of open source maintainers decided to clone the existing library into this repository. See [dgrijalva/jwt-go#462](https://github.com/dgrijalva/jwt-go/issues/462) for a detailed discussion on this topic. **NEW VERSION COMING:** There have been a lot of improvements suggested since the version 3.0.0 released in 2016. I'm working now on cutting two different releases: 3.2.0 will contain any non-breaking changes or enhancements. 4.0.0 will follow shortly which will include breaking changes. See the 4.0.0 milestone to get an idea of what's coming. If you have other ideas, or would like to participate in 4.0.0, now's the time. If you depend on this library and don't want to be interrupted, I recommend you use your dependency mangement tool to pin to version 3.
Future releases will be using the `github.com/golang-jwt/jwt` import path and continue the existing versioning scheme of `v3.x.x+incompatible`. Backwards-compatible patches and fixes will be done on the `v3` release branch, where as new build-breaking features will be developed in a `v4` release, possibly including a SIV-style import path. **SECURITY NOTICE:** Some older versions of Go have a security issue in the cryotp/elliptic. Recommendation is to upgrade to at least 1.8.3. See issue #216 for more detail.
**SECURITY NOTICE:** Some older versions of Go have a security issue in the crypto/elliptic. Recommendation is to upgrade to at least 1.15 See issue [dgrijalva/jwt-go#216](https://github.com/dgrijalva/jwt-go/issues/216) for more detail. **SECURITY NOTICE:** It's important that you [validate the `alg` presented is what you expect](https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/). This library attempts to make it easy to do the right thing by requiring key types match the expected alg, but you should take the extra step to verify it in your usage. See the examples provided.
**SECURITY NOTICE:** It's important that you [validate the `alg` presented is what you expect](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/). This library attempts to make it easy to do the right thing by requiring key types match the expected alg, but you should take the extra step to verify it in your usage. See the examples provided.
### Supported Go versions
Our support of Go versions is aligned with Go's [version release policy](https://golang.org/doc/devel/release#policy).
So we will support a major version of Go until there are two newer major releases.
We no longer support building jwt-go with unsupported Go versions, as these contain security vulnerabilities
which will not be fixed.
## What the heck is a JWT? ## What the heck is a JWT?
JWT.io has [a great introduction](https://jwt.io/introduction) to JSON Web Tokens. JWT.io has [a great introduction](https://jwt.io/introduction) to JSON Web Tokens.
In short, it's a signed JSON object that does something useful (for example, authentication). It's commonly used for `Bearer` tokens in Oauth 2. A token is made of three parts, separated by `.`'s. The first two parts are JSON objects, that have been [base64url](https://datatracker.ietf.org/doc/html/rfc4648) encoded. The last part is the signature, encoded the same way. In short, it's a signed JSON object that does something useful (for example, authentication). It's commonly used for `Bearer` tokens in Oauth 2. A token is made of three parts, separated by `.`'s. The first two parts are JSON objects, that have been [base64url](http://tools.ietf.org/html/rfc4648) encoded. The last part is the signature, encoded the same way.
The first part is called the header. It contains the necessary information for verifying the last part, the signature. For example, which encryption method was used for signing and what key was used. The first part is called the header. It contains the necessary information for verifying the last part, the signature. For example, which encryption method was used for signing and what key was used.
The part in the middle is the interesting bit. It's called the Claims and contains the actual stuff you care about. Refer to [RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519) for information about reserved keys and the proper way to add your own. The part in the middle is the interesting bit. It's called the Claims and contains the actual stuff you care about. Refer to [the RFC](http://self-issued.info/docs/draft-jones-json-web-token.html) for information about reserved keys and the proper way to add your own.
## What's in the box? ## What's in the box?
@ -36,31 +27,31 @@ This library supports the parsing and verification as well as the generation and
## Examples ## Examples
See [the project documentation](https://pkg.go.dev/github.com/golang-jwt/jwt) for examples of usage: See [the project documentation](https://godoc.org/github.com/dgrijalva/jwt-go) for examples of usage:
* [Simple example of parsing and validating a token](https://pkg.go.dev/github.com/golang-jwt/jwt#example-Parse-Hmac) * [Simple example of parsing and validating a token](https://godoc.org/github.com/dgrijalva/jwt-go#example-Parse--Hmac)
* [Simple example of building and signing a token](https://pkg.go.dev/github.com/golang-jwt/jwt#example-New-Hmac) * [Simple example of building and signing a token](https://godoc.org/github.com/dgrijalva/jwt-go#example-New--Hmac)
* [Directory of Examples](https://pkg.go.dev/github.com/golang-jwt/jwt#pkg-examples) * [Directory of Examples](https://godoc.org/github.com/dgrijalva/jwt-go#pkg-examples)
## Extensions ## Extensions
This library publishes all the necessary components for adding your own signing methods. Simply implement the `SigningMethod` interface and register a factory method using `RegisterSigningMethod`. This library publishes all the necessary components for adding your own signing methods. Simply implement the `SigningMethod` interface and register a factory method using `RegisterSigningMethod`.
Here's an example of an extension that integrates with multiple Google Cloud Platform signing tools (AppEngine, IAM API, Cloud KMS): https://github.com/someone1/gcp-jwt-go Here's an example of an extension that integrates with the Google App Engine signing tools: https://github.com/someone1/gcp-jwt-go
## Compliance ## Compliance
This library was last reviewed to comply with [RTF 7519](https://datatracker.ietf.org/doc/html/rfc7519) dated May 2015 with a few notable differences: This library was last reviewed to comply with [RTF 7519](http://www.rfc-editor.org/info/rfc7519) dated May 2015 with a few notable differences:
* In order to protect against accidental use of [Unsecured JWTs](https://datatracker.ietf.org/doc/html/rfc7519#section-6), tokens using `alg=none` will only be accepted if the constant `jwt.UnsafeAllowNoneSignatureType` is provided as the key. * In order to protect against accidental use of [Unsecured JWTs](http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#UnsecuredJWT), tokens using `alg=none` will only be accepted if the constant `jwt.UnsafeAllowNoneSignatureType` is provided as the key.
## Project Status & Versioning ## Project Status & Versioning
This library is considered production ready. Feedback and feature requests are appreciated. The API should be considered stable. There should be very few backwards-incompatible changes outside of major version updates (and only with good reason). This library is considered production ready. Feedback and feature requests are appreciated. The API should be considered stable. There should be very few backwards-incompatible changes outside of major version updates (and only with good reason).
This project uses [Semantic Versioning 2.0.0](http://semver.org). Accepted pull requests will land on `main`. Periodically, versions will be tagged from `main`. You can find all the releases on [the project releases page](https://github.com/golang-jwt/jwt/releases). This project uses [Semantic Versioning 2.0.0](http://semver.org). Accepted pull requests will land on `master`. Periodically, versions will be tagged from `master`. You can find all the releases on [the project releases page](https://github.com/dgrijalva/jwt-go/releases).
While we try to make it obvious when we make breaking changes, there isn't a great mechanism for pushing announcements out to users. You may want to use this alternative package include: `gopkg.in/golang-jwt/jwt.v3`. It will do the right thing WRT semantic versioning. While we try to make it obvious when we make breaking changes, there isn't a great mechanism for pushing announcements out to users. You may want to use this alternative package include: `gopkg.in/dgrijalva/jwt-go.v3`. It will do the right thing WRT semantic versioning.
**BREAKING CHANGES:*** **BREAKING CHANGES:***
* Version 3.0.0 includes _a lot_ of changes from the 2.x line, including a few that break the API. We've tried to break as few things as possible, so there should just be a few type signature changes. A full list of breaking changes is available in `VERSION_HISTORY.md`. See `MIGRATION_GUIDE.md` for more information on updating your code. * Version 3.0.0 includes _a lot_ of changes from the 2.x line, including a few that break the API. We've tried to break as few things as possible, so there should just be a few type signature changes. A full list of breaking changes is available in `VERSION_HISTORY.md`. See `MIGRATION_GUIDE.md` for more information on updating your code.
@ -88,9 +79,9 @@ Asymmetric signing methods, such as RSA, use different keys for signing and veri
Each signing method expects a different object type for its signing keys. See the package documentation for details. Here are the most common ones: Each signing method expects a different object type for its signing keys. See the package documentation for details. Here are the most common ones:
* The [HMAC signing method](https://pkg.go.dev/github.com/golang-jwt/jwt#SigningMethodHMAC) (`HS256`,`HS384`,`HS512`) expect `[]byte` values for signing and validation * The [HMAC signing method](https://godoc.org/github.com/dgrijalva/jwt-go#SigningMethodHMAC) (`HS256`,`HS384`,`HS512`) expect `[]byte` values for signing and validation
* The [RSA signing method](https://pkg.go.dev/github.com/golang-jwt/jwt#SigningMethodRSA) (`RS256`,`RS384`,`RS512`) expect `*rsa.PrivateKey` for signing and `*rsa.PublicKey` for validation * The [RSA signing method](https://godoc.org/github.com/dgrijalva/jwt-go#SigningMethodRSA) (`RS256`,`RS384`,`RS512`) expect `*rsa.PrivateKey` for signing and `*rsa.PublicKey` for validation
* The [ECDSA signing method](https://pkg.go.dev/github.com/golang-jwt/jwt#SigningMethodECDSA) (`ES256`,`ES384`,`ES512`) expect `*ecdsa.PrivateKey` for signing and `*ecdsa.PublicKey` for validation * The [ECDSA signing method](https://godoc.org/github.com/dgrijalva/jwt-go#SigningMethodECDSA) (`ES256`,`ES384`,`ES512`) expect `*ecdsa.PrivateKey` for signing and `*ecdsa.PublicKey` for validation
### JWT and OAuth ### JWT and OAuth
@ -102,12 +93,8 @@ Without going too far down the rabbit hole, here's a description of the interact
* OAuth defines several options for passing around authentication data. One popular method is called a "bearer token". A bearer token is simply a string that _should_ only be held by an authenticated user. Thus, simply presenting this token proves your identity. You can probably derive from here why a JWT might make a good bearer token. * OAuth defines several options for passing around authentication data. One popular method is called a "bearer token". A bearer token is simply a string that _should_ only be held by an authenticated user. Thus, simply presenting this token proves your identity. You can probably derive from here why a JWT might make a good bearer token.
* Because bearer tokens are used for authentication, it's important they're kept secret. This is why transactions that use bearer tokens typically happen over SSL. * Because bearer tokens are used for authentication, it's important they're kept secret. This is why transactions that use bearer tokens typically happen over SSL.
### Troubleshooting
This library uses descriptive error messages whenever possible. If you are not getting the expected result, have a look at the errors. The most common place people get stuck is providing the correct type of key to the parser. See the above section on signing methods and key types.
## More ## More
Documentation can be found [on pkg.go.dev](https://pkg.go.dev/github.com/golang-jwt/jwt). Documentation can be found [on godoc.org](http://godoc.org/github.com/dgrijalva/jwt-go).
The command line utility included in this project (cmd/jwt) provides a straightforward example of token creation and parsing as well as a useful tool for debugging your own integration. You'll also find several implementation examples in the documentation. The command line utility included in this project (cmd/jwt) provides a straightforward example of token creation and parsing as well as a useful tool for debugging your own integration. You'll also find several implementation examples in the documentation.

View File

@ -1,18 +1,5 @@
## `jwt-go` Version History ## `jwt-go` Version History
#### 3.2.2
* Starting from this release, we are adopting the policy to support the most 2 recent versions of Go currently available. By the time of this release, this is Go 1.15 and 1.16 ([#28](https://github.com/golang-jwt/jwt/pull/28)).
* Fixed a potential issue that could occur when the verification of `exp`, `iat` or `nbf` was not required and contained invalid contents, i.e. non-numeric/date. Thanks for @thaJeztah for making us aware of that and @giorgos-f3 for originally reporting it to the formtech fork ([#40](https://github.com/golang-jwt/jwt/pull/40)).
* Added support for EdDSA / ED25519 ([#36](https://github.com/golang-jwt/jwt/pull/36)).
* Optimized allocations ([#33](https://github.com/golang-jwt/jwt/pull/33)).
#### 3.2.1
* **Import Path Change**: See MIGRATION_GUIDE.md for tips on updating your code
* Changed the import path from `github.com/dgrijalva/jwt-go` to `github.com/golang-jwt/jwt`
* Fixed type confusing issue between `string` and `[]string` in `VerifyAudience` ([#12](https://github.com/golang-jwt/jwt/pull/12)). This fixes CVE-2020-26160
#### 3.2.0 #### 3.2.0
* Added method `ParseUnverified` to allow users to split up the tasks of parsing and validation * Added method `ParseUnverified` to allow users to split up the tasks of parsing and validation

View File

@ -35,18 +35,18 @@ func (c StandardClaims) Valid() error {
// The claims below are optional, by default, so if they are set to the // The claims below are optional, by default, so if they are set to the
// default value in Go, let's not fail the verification for them. // default value in Go, let's not fail the verification for them.
if !c.VerifyExpiresAt(now, false) { if c.VerifyExpiresAt(now, false) == false {
delta := time.Unix(now, 0).Sub(time.Unix(c.ExpiresAt, 0)) delta := time.Unix(now, 0).Sub(time.Unix(c.ExpiresAt, 0))
vErr.Inner = fmt.Errorf("token is expired by %v", delta) vErr.Inner = fmt.Errorf("token is expired by %v", delta)
vErr.Errors |= ValidationErrorExpired vErr.Errors |= ValidationErrorExpired
} }
if !c.VerifyIssuedAt(now, false) { if c.VerifyIssuedAt(now, false) == false {
vErr.Inner = fmt.Errorf("Token used before issued") vErr.Inner = fmt.Errorf("Token used before issued")
vErr.Errors |= ValidationErrorIssuedAt vErr.Errors |= ValidationErrorIssuedAt
} }
if !c.VerifyNotBefore(now, false) { if c.VerifyNotBefore(now, false) == false {
vErr.Inner = fmt.Errorf("token is not valid yet") vErr.Inner = fmt.Errorf("token is not valid yet")
vErr.Errors |= ValidationErrorNotValidYet vErr.Errors |= ValidationErrorNotValidYet
} }
@ -61,7 +61,7 @@ func (c StandardClaims) Valid() error {
// Compares the aud claim against cmp. // Compares the aud claim against cmp.
// If required is false, this method will return true if the value matches or is unset // If required is false, this method will return true if the value matches or is unset
func (c *StandardClaims) VerifyAudience(cmp string, req bool) bool { func (c *StandardClaims) VerifyAudience(cmp string, req bool) bool {
return verifyAud([]string{c.Audience}, cmp, req) return verifyAud(c.Audience, cmp, req)
} }
// Compares the exp claim against cmp. // Compares the exp claim against cmp.
@ -90,27 +90,15 @@ func (c *StandardClaims) VerifyNotBefore(cmp int64, req bool) bool {
// ----- helpers // ----- helpers
func verifyAud(aud []string, cmp string, required bool) bool { func verifyAud(aud string, cmp string, required bool) bool {
if len(aud) == 0 { if aud == "" {
return !required return !required
} }
// use a var here to keep constant time compare when looping over a number of claims if subtle.ConstantTimeCompare([]byte(aud), []byte(cmp)) != 0 {
result := false return true
} else {
var stringClaims string return false
for _, a := range aud {
if subtle.ConstantTimeCompare([]byte(a), []byte(cmp)) != 0 {
result = true
}
stringClaims = stringClaims + a
} }
// case where "" is sent in one or many aud claims
if len(stringClaims) == 0 {
return !required
}
return result
} }
func verifyExp(exp int64, now int64, required bool) bool { func verifyExp(exp int64, now int64, required bool) bool {

View File

@ -88,11 +88,11 @@ func (m *SigningMethodECDSA) Verify(signingString, signature string, key interfa
hasher.Write([]byte(signingString)) hasher.Write([]byte(signingString))
// Verify the signature // Verify the signature
if verifystatus := ecdsa.Verify(ecdsaKey, hasher.Sum(nil), r, s); verifystatus { if verifystatus := ecdsa.Verify(ecdsaKey, hasher.Sum(nil), r, s); verifystatus == true {
return nil return nil
} else {
return ErrECDSAVerification
} }
return ErrECDSAVerification
} }
// Implements the Sign method from SigningMethod // Implements the Sign method from SigningMethod
@ -128,12 +128,18 @@ func (m *SigningMethodECDSA) Sign(signingString string, key interface{}) (string
keyBytes += 1 keyBytes += 1
} }
// We serialize the outputs (r and s) into big-endian byte arrays // We serialize the outpus (r and s) into big-endian byte arrays and pad
// padded with zeros on the left to make sure the sizes work out. // them with zeros on the left to make sure the sizes work out. Both arrays
// Output must be 2*keyBytes long. // must be keyBytes long, and the output must be 2*keyBytes long.
out := make([]byte, 2*keyBytes) rBytes := r.Bytes()
r.FillBytes(out[0:keyBytes]) // r is assigned to the first half of output. rBytesPadded := make([]byte, keyBytes)
s.FillBytes(out[keyBytes:]) // s is assigned to the second half of output. copy(rBytesPadded[keyBytes-len(rBytes):], rBytes)
sBytes := s.Bytes()
sBytesPadded := make([]byte, keyBytes)
copy(sBytesPadded[keyBytes-len(sBytes):], sBytes)
out := append(rBytesPadded, sBytesPadded...)
return EncodeSegment(out), nil return EncodeSegment(out), nil
} else { } else {

View File

@ -25,9 +25,7 @@ func ParseECPrivateKeyFromPEM(key []byte) (*ecdsa.PrivateKey, error) {
// Parse the key // Parse the key
var parsedKey interface{} var parsedKey interface{}
if parsedKey, err = x509.ParseECPrivateKey(block.Bytes); err != nil { if parsedKey, err = x509.ParseECPrivateKey(block.Bytes); err != nil {
if parsedKey, err = x509.ParsePKCS8PrivateKey(block.Bytes); err != nil { return nil, err
return nil, err
}
} }
var pkey *ecdsa.PrivateKey var pkey *ecdsa.PrivateKey

View File

@ -10,59 +10,37 @@ import (
// This is the default claims type if you don't supply one // This is the default claims type if you don't supply one
type MapClaims map[string]interface{} type MapClaims map[string]interface{}
// VerifyAudience Compares the aud claim against cmp. // Compares the aud claim against cmp.
// If required is false, this method will return true if the value matches or is unset // If required is false, this method will return true if the value matches or is unset
func (m MapClaims) VerifyAudience(cmp string, req bool) bool { func (m MapClaims) VerifyAudience(cmp string, req bool) bool {
var aud []string aud, _ := m["aud"].(string)
switch v := m["aud"].(type) {
case string:
aud = append(aud, v)
case []string:
aud = v
case []interface{}:
for _, a := range v {
vs, ok := a.(string)
if !ok {
return false
}
aud = append(aud, vs)
}
}
return verifyAud(aud, cmp, req) return verifyAud(aud, cmp, req)
} }
// Compares the exp claim against cmp. // Compares the exp claim against cmp.
// If required is false, this method will return true if the value matches or is unset // If required is false, this method will return true if the value matches or is unset
func (m MapClaims) VerifyExpiresAt(cmp int64, req bool) bool { func (m MapClaims) VerifyExpiresAt(cmp int64, req bool) bool {
exp, ok := m["exp"] switch exp := m["exp"].(type) {
if !ok {
return !req
}
switch expType := exp.(type) {
case float64: case float64:
return verifyExp(int64(expType), cmp, req) return verifyExp(int64(exp), cmp, req)
case json.Number: case json.Number:
v, _ := expType.Int64() v, _ := exp.Int64()
return verifyExp(v, cmp, req) return verifyExp(v, cmp, req)
} }
return false return req == false
} }
// Compares the iat claim against cmp. // Compares the iat claim against cmp.
// If required is false, this method will return true if the value matches or is unset // If required is false, this method will return true if the value matches or is unset
func (m MapClaims) VerifyIssuedAt(cmp int64, req bool) bool { func (m MapClaims) VerifyIssuedAt(cmp int64, req bool) bool {
iat, ok := m["iat"] switch iat := m["iat"].(type) {
if !ok {
return !req
}
switch iatType := iat.(type) {
case float64: case float64:
return verifyIat(int64(iatType), cmp, req) return verifyIat(int64(iat), cmp, req)
case json.Number: case json.Number:
v, _ := iatType.Int64() v, _ := iat.Int64()
return verifyIat(v, cmp, req) return verifyIat(v, cmp, req)
} }
return false return req == false
} }
// Compares the iss claim against cmp. // Compares the iss claim against cmp.
@ -75,18 +53,14 @@ func (m MapClaims) VerifyIssuer(cmp string, req bool) bool {
// Compares the nbf claim against cmp. // Compares the nbf claim against cmp.
// If required is false, this method will return true if the value matches or is unset // If required is false, this method will return true if the value matches or is unset
func (m MapClaims) VerifyNotBefore(cmp int64, req bool) bool { func (m MapClaims) VerifyNotBefore(cmp int64, req bool) bool {
nbf, ok := m["nbf"] switch nbf := m["nbf"].(type) {
if !ok {
return !req
}
switch nbfType := nbf.(type) {
case float64: case float64:
return verifyNbf(int64(nbfType), cmp, req) return verifyNbf(int64(nbf), cmp, req)
case json.Number: case json.Number:
v, _ := nbfType.Int64() v, _ := nbf.Int64()
return verifyNbf(v, cmp, req) return verifyNbf(v, cmp, req)
} }
return false return req == false
} }
// Validates time based claims "exp, iat, nbf". // Validates time based claims "exp, iat, nbf".
@ -97,17 +71,17 @@ func (m MapClaims) Valid() error {
vErr := new(ValidationError) vErr := new(ValidationError)
now := TimeFunc().Unix() now := TimeFunc().Unix()
if !m.VerifyExpiresAt(now, false) { if m.VerifyExpiresAt(now, false) == false {
vErr.Inner = errors.New("Token is expired") vErr.Inner = errors.New("Token is expired")
vErr.Errors |= ValidationErrorExpired vErr.Errors |= ValidationErrorExpired
} }
if !m.VerifyIssuedAt(now, false) { if m.VerifyIssuedAt(now, false) == false {
vErr.Inner = errors.New("Token used before issued") vErr.Inner = errors.New("Token used before issued")
vErr.Errors |= ValidationErrorIssuedAt vErr.Errors |= ValidationErrorIssuedAt
} }
if !m.VerifyNotBefore(now, false) { if m.VerifyNotBefore(now, false) == false {
vErr.Inner = errors.New("Token is not valid yet") vErr.Inner = errors.New("Token is not valid yet")
vErr.Errors |= ValidationErrorNotValidYet vErr.Errors |= ValidationErrorNotValidYet
} }

View File

@ -12,14 +12,9 @@ import (
type SigningMethodRSAPSS struct { type SigningMethodRSAPSS struct {
*SigningMethodRSA *SigningMethodRSA
Options *rsa.PSSOptions Options *rsa.PSSOptions
// VerifyOptions is optional. If set overrides Options for rsa.VerifyPPS.
// Used to accept tokens signed with rsa.PSSSaltLengthAuto, what doesn't follow
// https://tools.ietf.org/html/rfc7518#section-3.5 but was used previously.
// See https://github.com/dgrijalva/jwt-go/issues/285#issuecomment-437451244 for details.
VerifyOptions *rsa.PSSOptions
} }
// Specific instances for RS/PS and company. // Specific instances for RS/PS and company
var ( var (
SigningMethodPS256 *SigningMethodRSAPSS SigningMethodPS256 *SigningMethodRSAPSS
SigningMethodPS384 *SigningMethodRSAPSS SigningMethodPS384 *SigningMethodRSAPSS
@ -29,15 +24,13 @@ var (
func init() { func init() {
// PS256 // PS256
SigningMethodPS256 = &SigningMethodRSAPSS{ SigningMethodPS256 = &SigningMethodRSAPSS{
SigningMethodRSA: &SigningMethodRSA{ &SigningMethodRSA{
Name: "PS256", Name: "PS256",
Hash: crypto.SHA256, Hash: crypto.SHA256,
}, },
Options: &rsa.PSSOptions{ &rsa.PSSOptions{
SaltLength: rsa.PSSSaltLengthEqualsHash,
},
VerifyOptions: &rsa.PSSOptions{
SaltLength: rsa.PSSSaltLengthAuto, SaltLength: rsa.PSSSaltLengthAuto,
Hash: crypto.SHA256,
}, },
} }
RegisterSigningMethod(SigningMethodPS256.Alg(), func() SigningMethod { RegisterSigningMethod(SigningMethodPS256.Alg(), func() SigningMethod {
@ -46,15 +39,13 @@ func init() {
// PS384 // PS384
SigningMethodPS384 = &SigningMethodRSAPSS{ SigningMethodPS384 = &SigningMethodRSAPSS{
SigningMethodRSA: &SigningMethodRSA{ &SigningMethodRSA{
Name: "PS384", Name: "PS384",
Hash: crypto.SHA384, Hash: crypto.SHA384,
}, },
Options: &rsa.PSSOptions{ &rsa.PSSOptions{
SaltLength: rsa.PSSSaltLengthEqualsHash,
},
VerifyOptions: &rsa.PSSOptions{
SaltLength: rsa.PSSSaltLengthAuto, SaltLength: rsa.PSSSaltLengthAuto,
Hash: crypto.SHA384,
}, },
} }
RegisterSigningMethod(SigningMethodPS384.Alg(), func() SigningMethod { RegisterSigningMethod(SigningMethodPS384.Alg(), func() SigningMethod {
@ -63,15 +54,13 @@ func init() {
// PS512 // PS512
SigningMethodPS512 = &SigningMethodRSAPSS{ SigningMethodPS512 = &SigningMethodRSAPSS{
SigningMethodRSA: &SigningMethodRSA{ &SigningMethodRSA{
Name: "PS512", Name: "PS512",
Hash: crypto.SHA512, Hash: crypto.SHA512,
}, },
Options: &rsa.PSSOptions{ &rsa.PSSOptions{
SaltLength: rsa.PSSSaltLengthEqualsHash,
},
VerifyOptions: &rsa.PSSOptions{
SaltLength: rsa.PSSSaltLengthAuto, SaltLength: rsa.PSSSaltLengthAuto,
Hash: crypto.SHA512,
}, },
} }
RegisterSigningMethod(SigningMethodPS512.Alg(), func() SigningMethod { RegisterSigningMethod(SigningMethodPS512.Alg(), func() SigningMethod {
@ -105,12 +94,7 @@ func (m *SigningMethodRSAPSS) Verify(signingString, signature string, key interf
hasher := m.Hash.New() hasher := m.Hash.New()
hasher.Write([]byte(signingString)) hasher.Write([]byte(signingString))
opts := m.Options return rsa.VerifyPSS(rsaKey, m.Hash, hasher.Sum(nil), sig, m.Options)
if m.VerifyOptions != nil {
opts = m.VerifyOptions
}
return rsa.VerifyPSS(rsaKey, m.Hash, hasher.Sum(nil), sig, opts)
} }
// Implements the Sign method from SigningMethod // Implements the Sign method from SigningMethod

View File

@ -8,7 +8,7 @@ import (
) )
var ( var (
ErrKeyMustBePEMEncoded = errors.New("Invalid Key: Key must be a PEM encoded PKCS1 or PKCS8 key") ErrKeyMustBePEMEncoded = errors.New("Invalid Key: Key must be PEM encoded PKCS1 or PKCS8 private key")
ErrNotRSAPrivateKey = errors.New("Key is not a valid RSA private key") ErrNotRSAPrivateKey = errors.New("Key is not a valid RSA private key")
ErrNotRSAPublicKey = errors.New("Key is not a valid RSA public key") ErrNotRSAPublicKey = errors.New("Key is not a valid RSA public key")
) )

View File

@ -65,7 +65,7 @@ func (t *Token) SignedString(key interface{}) (string, error) {
func (t *Token) SigningString() (string, error) { func (t *Token) SigningString() (string, error) {
var err error var err error
parts := make([]string, 2) parts := make([]string, 2)
for i := range parts { for i, _ := range parts {
var jsonValue []byte var jsonValue []byte
if i == 0 { if i == 0 {
if jsonValue, err = json.Marshal(t.Header); err != nil { if jsonValue, err = json.Marshal(t.Header); err != nil {
@ -95,10 +95,14 @@ func ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token
// Encode JWT specific base64url encoding with padding stripped // Encode JWT specific base64url encoding with padding stripped
func EncodeSegment(seg []byte) string { func EncodeSegment(seg []byte) string {
return base64.RawURLEncoding.EncodeToString(seg) return strings.TrimRight(base64.URLEncoding.EncodeToString(seg), "=")
} }
// Decode JWT specific base64url encoding with padding stripped // Decode JWT specific base64url encoding with padding stripped
func DecodeSegment(seg string) ([]byte, error) { func DecodeSegment(seg string) ([]byte, error) {
return base64.RawURLEncoding.DecodeString(seg) if l := len(seg) % 4; l > 0 {
seg += strings.Repeat("=", 4-l)
}
return base64.URLEncoding.DecodeString(seg)
} }

View File

@ -16,7 +16,7 @@ func (a *AccountService) New(req acme.Account) (acme.ExtendedAccount, error) {
resp, err := a.core.post(a.core.GetDirectory().NewAccountURL, req, &account) resp, err := a.core.post(a.core.GetDirectory().NewAccountURL, req, &account)
location := getLocation(resp) location := getLocation(resp)
if location != "" { if len(location) > 0 {
a.core.jws.SetKid(location) a.core.jws.SetKid(location)
} }
@ -46,7 +46,7 @@ func (a *AccountService) NewEAB(accMsg acme.Account, kid, hmacEncoded string) (a
// Get Retrieves an account. // Get Retrieves an account.
func (a *AccountService) Get(accountURL string) (acme.Account, error) { func (a *AccountService) Get(accountURL string) (acme.Account, error) {
if accountURL == "" { if len(accountURL) == 0 {
return acme.Account{}, errors.New("account[get]: empty URL") return acme.Account{}, errors.New("account[get]: empty URL")
} }
@ -60,7 +60,7 @@ func (a *AccountService) Get(accountURL string) (acme.Account, error) {
// Update Updates an account. // Update Updates an account.
func (a *AccountService) Update(accountURL string, req acme.Account) (acme.Account, error) { func (a *AccountService) Update(accountURL string, req acme.Account) (acme.Account, error) {
if accountURL == "" { if len(accountURL) == 0 {
return acme.Account{}, errors.New("account[update]: empty URL") return acme.Account{}, errors.New("account[update]: empty URL")
} }
@ -75,7 +75,7 @@ func (a *AccountService) Update(accountURL string, req acme.Account) (acme.Accou
// Deactivate Deactivates an account. // Deactivate Deactivates an account.
func (a *AccountService) Deactivate(accountURL string) error { func (a *AccountService) Deactivate(accountURL string) error {
if accountURL == "" { if len(accountURL) == 0 {
return errors.New("account[deactivate]: empty URL") return errors.New("account[deactivate]: empty URL")
} }

View File

@ -2,6 +2,7 @@ package api
import ( import (
"bytes" "bytes"
"context"
"crypto" "crypto"
"encoding/json" "encoding/json"
"errors" "errors"
@ -70,7 +71,7 @@ func (a *Core) post(uri string, reqBody, response interface{}) (*http.Response,
} }
// postAsGet performs an HTTP POST ("POST-as-GET") request. // postAsGet performs an HTTP POST ("POST-as-GET") request.
// https://www.rfc-editor.org/rfc/rfc8555.html#section-6.3 // https://tools.ietf.org/html/rfc8555#section-6.3
func (a *Core) postAsGet(uri string, response interface{}) (*http.Response, error) { func (a *Core) postAsGet(uri string, response interface{}) (*http.Response, error) {
return a.retrievablePost(uri, []byte{}, response) return a.retrievablePost(uri, []byte{}, response)
} }
@ -82,6 +83,8 @@ func (a *Core) retrievablePost(uri string, content []byte, response interface{})
bo.MaxInterval = 5 * time.Second bo.MaxInterval = 5 * time.Second
bo.MaxElapsedTime = 20 * time.Second bo.MaxElapsedTime = 20 * time.Second
ctx, cancel := context.WithCancel(context.Background())
var resp *http.Response var resp *http.Response
operation := func() error { operation := func() error {
var err error var err error
@ -93,7 +96,8 @@ func (a *Core) retrievablePost(uri string, content []byte, response interface{})
return err return err
} }
return backoff.Permanent(err) cancel()
return err
} }
return nil return nil
@ -103,7 +107,7 @@ func (a *Core) retrievablePost(uri string, content []byte, response interface{})
log.Infof("retry due to: %v", err) log.Infof("retry due to: %v", err)
} }
err := backoff.RetryNotify(operation, bo, notify) err := backoff.RetryNotify(operation, backoff.WithContext(bo, ctx), notify)
if err != nil { if err != nil {
return resp, err return resp, err
} }
@ -117,7 +121,7 @@ func (a *Core) signedPost(uri string, content []byte, response interface{}) (*ht
return nil, fmt.Errorf("failed to post JWS message: failed to sign content: %w", err) return nil, fmt.Errorf("failed to post JWS message: failed to sign content: %w", err)
} }
signedBody := bytes.NewBufferString(signedContent.FullSerialize()) signedBody := bytes.NewBuffer([]byte(signedContent.FullSerialize()))
resp, err := a.doer.Post(uri, signedBody, "application/jose+json", response) resp, err := a.doer.Post(uri, signedBody, "application/jose+json", response)

View File

@ -10,7 +10,7 @@ type AuthorizationService service
// Get Gets an authorization. // Get Gets an authorization.
func (c *AuthorizationService) Get(authzURL string) (acme.Authorization, error) { func (c *AuthorizationService) Get(authzURL string) (acme.Authorization, error) {
if authzURL == "" { if len(authzURL) == 0 {
return acme.Authorization{}, errors.New("authorization[get]: empty URL") return acme.Authorization{}, errors.New("authorization[get]: empty URL")
} }
@ -24,7 +24,7 @@ func (c *AuthorizationService) Get(authzURL string) (acme.Authorization, error)
// Deactivate Deactivates an authorization. // Deactivate Deactivates an authorization.
func (c *AuthorizationService) Deactivate(authzURL string) error { func (c *AuthorizationService) Deactivate(authzURL string) error {
if authzURL == "" { if len(authzURL) == 0 {
return errors.New("authorization[deactivate]: empty URL") return errors.New("authorization[deactivate]: empty URL")
} }

View File

@ -1,11 +1,10 @@
package api package api
import ( import (
"bytes"
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"errors" "errors"
"io" "io/ioutil"
"net/http" "net/http"
"github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme"
@ -21,87 +20,19 @@ type CertificateService service
// Get Returns the certificate and the issuer certificate. // Get Returns the certificate and the issuer certificate.
// 'bundle' is only applied if the issuer is provided by the 'up' link. // 'bundle' is only applied if the issuer is provided by the 'up' link.
func (c *CertificateService) Get(certURL string, bundle bool) ([]byte, []byte, error) { func (c *CertificateService) Get(certURL string, bundle bool) ([]byte, []byte, error) {
cert, _, err := c.get(certURL, bundle) cert, up, err := c.get(certURL)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
return cert.Cert, cert.Issuer, nil
}
// GetAll the certificates and the alternate certificates.
// bundle' is only applied if the issuer is provided by the 'up' link.
func (c *CertificateService) GetAll(certURL string, bundle bool) (map[string]*acme.RawCertificate, error) {
cert, headers, err := c.get(certURL, bundle)
if err != nil {
return nil, err
}
certs := map[string]*acme.RawCertificate{certURL: cert}
// URLs of "alternate" link relation
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4.2
alts := getLinks(headers, "alternate")
for _, alt := range alts {
altCert, _, err := c.get(alt, bundle)
if err != nil {
return nil, err
}
certs[alt] = altCert
}
return certs, nil
}
// Revoke Revokes a certificate.
func (c *CertificateService) Revoke(req acme.RevokeCertMessage) error {
_, err := c.core.post(c.core.GetDirectory().RevokeCertURL, req, nil)
return err
}
// get Returns the certificate and the "up" link.
func (c *CertificateService) get(certURL string, bundle bool) (*acme.RawCertificate, http.Header, error) {
if certURL == "" {
return nil, nil, errors.New("certificate[get]: empty URL")
}
resp, err := c.core.postAsGet(certURL, nil)
if err != nil {
return nil, nil, err
}
data, err := io.ReadAll(http.MaxBytesReader(nil, resp.Body, maxBodySize))
if err != nil {
return nil, resp.Header, err
}
cert := c.getCertificateChain(data, resp.Header, bundle, certURL)
return cert, resp.Header, err
}
// getCertificateChain Returns the certificate and the issuer certificate.
func (c *CertificateService) getCertificateChain(cert []byte, headers http.Header, bundle bool, certURL string) *acme.RawCertificate {
// Get issuerCert from bundled response from Let's Encrypt // Get issuerCert from bundled response from Let's Encrypt
// See https://community.letsencrypt.org/t/acme-v2-no-up-link-in-response/64962 // See https://community.letsencrypt.org/t/acme-v2-no-up-link-in-response/64962
_, issuer := pem.Decode(cert) _, issuer := pem.Decode(cert)
if issuer != nil { if issuer != nil {
// If bundle is false, we want to return a single certificate. return cert, issuer, nil
// To do this, we remove the issuer cert(s) from the issued cert.
if !bundle {
cert = bytes.TrimSuffix(cert, issuer)
}
return &acme.RawCertificate{Cert: cert, Issuer: issuer}
} }
// The issuer certificate link may be supplied via an "up" link issuer, err = c.getIssuerFromLink(up)
// in the response headers of a new certificate.
// See https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4.2
up := getLink(headers, "up")
issuer, err := c.getIssuerFromLink(up)
if err != nil { if err != nil {
// If we fail to acquire the issuer cert, return the issued certificate - do not fail. // If we fail to acquire the issuer cert, return the issued certificate - do not fail.
log.Warnf("acme: Could not bundle issuer certificate [%s]: %v", certURL, err) log.Warnf("acme: Could not bundle issuer certificate [%s]: %v", certURL, err)
@ -113,26 +44,56 @@ func (c *CertificateService) getCertificateChain(cert []byte, headers http.Heade
} }
} }
return &acme.RawCertificate{Cert: cert, Issuer: issuer} return cert, issuer, nil
}
// Revoke Revokes a certificate.
func (c *CertificateService) Revoke(req acme.RevokeCertMessage) error {
_, err := c.core.post(c.core.GetDirectory().RevokeCertURL, req, nil)
return err
}
// get Returns the certificate and the "up" link.
func (c *CertificateService) get(certURL string) ([]byte, string, error) {
if len(certURL) == 0 {
return nil, "", errors.New("certificate[get]: empty URL")
}
resp, err := c.core.postAsGet(certURL, nil)
if err != nil {
return nil, "", err
}
cert, err := ioutil.ReadAll(http.MaxBytesReader(nil, resp.Body, maxBodySize))
if err != nil {
return nil, "", err
}
// The issuer certificate link may be supplied via an "up" link
// in the response headers of a new certificate.
// See https://tools.ietf.org/html/rfc8555#section-7.4.2
up := getLink(resp.Header, "up")
return cert, up, err
} }
// getIssuerFromLink requests the issuer certificate. // getIssuerFromLink requests the issuer certificate.
func (c *CertificateService) getIssuerFromLink(up string) ([]byte, error) { func (c *CertificateService) getIssuerFromLink(up string) ([]byte, error) {
if up == "" { if len(up) == 0 {
return nil, nil return nil, nil
} }
log.Infof("acme: Requesting issuer cert from %s", up) log.Infof("acme: Requesting issuer cert from %s", up)
cert, _, err := c.get(up, false) cert, _, err := c.get(up)
if err != nil { if err != nil {
return nil, err return nil, err
} }
_, err = x509.ParseCertificate(cert.Cert) _, err = x509.ParseCertificate(cert)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return certcrypto.PEMEncode(certcrypto.DERCertificateBytes(cert.Cert)), nil return certcrypto.PEMEncode(certcrypto.DERCertificateBytes(cert)), nil
} }

View File

@ -10,7 +10,7 @@ type ChallengeService service
// New Creates a challenge. // New Creates a challenge.
func (c *ChallengeService) New(chlgURL string) (acme.ExtendedChallenge, error) { func (c *ChallengeService) New(chlgURL string) (acme.ExtendedChallenge, error) {
if chlgURL == "" { if len(chlgURL) == 0 {
return acme.ExtendedChallenge{}, errors.New("challenge[new]: empty URL") return acme.ExtendedChallenge{}, errors.New("challenge[new]: empty URL")
} }
@ -29,7 +29,7 @@ func (c *ChallengeService) New(chlgURL string) (acme.ExtendedChallenge, error) {
// Get Gets a challenge. // Get Gets a challenge.
func (c *ChallengeService) Get(chlgURL string) (acme.ExtendedChallenge, error) { func (c *ChallengeService) Get(chlgURL string) (acme.ExtendedChallenge, error) {
if chlgURL == "" { if len(chlgURL) == 0 {
return acme.ExtendedChallenge{}, errors.New("challenge[get]: empty URL") return acme.ExtendedChallenge{}, errors.New("challenge[get]: empty URL")
} }

View File

@ -63,7 +63,7 @@ func (n *Manager) getNonce() (string, error) {
return GetFromResponse(resp) return GetFromResponse(resp)
} }
// GetFromResponse Extracts a nonce from an HTTP response. // GetFromResponse Extracts a nonce from a HTTP response.
func GetFromResponse(resp *http.Response) (string, error) { func GetFromResponse(resp *http.Response) (string, error) {
if resp == nil { if resp == nil {
return "", errors.New("nil response") return "", errors.New("nil response")

View File

@ -9,7 +9,7 @@ import (
"fmt" "fmt"
"github.com/go-acme/lego/v4/acme/api/internal/nonces" "github.com/go-acme/lego/v4/acme/api/internal/nonces"
jose "github.com/go-jose/go-jose/v4" jose "gopkg.in/square/go-jose.v2"
) )
// JWS Represents a JWS. // JWS Represents a JWS.

View File

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"runtime" "runtime"
"strings" "strings"
@ -95,7 +96,7 @@ func (d *Doer) do(req *http.Request, response interface{}) (*http.Response, erro
} }
if response != nil { if response != nil {
raw, err := io.ReadAll(resp.Body) raw, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
return resp, err return resp, err
} }
@ -119,7 +120,7 @@ func (d *Doer) formatUserAgent() string {
func checkError(req *http.Request, resp *http.Response) error { func checkError(req *http.Request, resp *http.Response) error {
if resp.StatusCode >= http.StatusBadRequest { if resp.StatusCode >= http.StatusBadRequest {
body, err := io.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
return fmt.Errorf("%d :: %s :: %s :: %w", resp.StatusCode, req.Method, req.URL, err) return fmt.Errorf("%d :: %s :: %s :: %w", resp.StatusCode, req.Method, req.URL, err)
} }
@ -133,10 +134,6 @@ func checkError(req *http.Request, resp *http.Response) error {
errorDetails.Method = req.Method errorDetails.Method = req.Method
errorDetails.URL = req.URL.String() errorDetails.URL = req.URL.String()
if errorDetails.HTTPStatus == 0 {
errorDetails.HTTPStatus = resp.StatusCode
}
// Check for errors we handle specifically // Check for errors we handle specifically
if errorDetails.HTTPStatus == http.StatusBadRequest && errorDetails.Type == acme.BadNonceErr { if errorDetails.HTTPStatus == http.StatusBadRequest && errorDetails.Type == acme.BadNonceErr {
return &acme.NonceError{ProblemDetails: errorDetails} return &acme.NonceError{ProblemDetails: errorDetails}

View File

@ -5,7 +5,7 @@ package sender
const ( const (
// ourUserAgent is the User-Agent of this underlying library package. // ourUserAgent is the User-Agent of this underlying library package.
ourUserAgent = "xenolf-acme/4.17.4" ourUserAgent = "xenolf-acme/4.1.0"
// ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package.
// values: detach|release // values: detach|release

View File

@ -3,58 +3,21 @@ package api
import ( import (
"encoding/base64" "encoding/base64"
"errors" "errors"
"net"
"time"
"github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme"
) )
// OrderOptions used to create an order (optional).
type OrderOptions struct {
NotBefore time.Time
NotAfter time.Time
// A string uniquely identifying a previously-issued certificate which this
// order is intended to replace.
// - https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5
ReplacesCertID string
}
type OrderService service type OrderService service
// New Creates a new order. // New Creates a new order.
func (o *OrderService) New(domains []string) (acme.ExtendedOrder, error) { func (o *OrderService) New(domains []string) (acme.ExtendedOrder, error) {
return o.NewWithOptions(domains, nil)
}
// NewWithOptions Creates a new order.
func (o *OrderService) NewWithOptions(domains []string, opts *OrderOptions) (acme.ExtendedOrder, error) {
var identifiers []acme.Identifier var identifiers []acme.Identifier
for _, domain := range domains { for _, domain := range domains {
ident := acme.Identifier{Value: domain, Type: "dns"} identifiers = append(identifiers, acme.Identifier{Type: "dns", Value: domain})
if net.ParseIP(domain) != nil {
ident.Type = "ip"
}
identifiers = append(identifiers, ident)
} }
orderReq := acme.Order{Identifiers: identifiers} orderReq := acme.Order{Identifiers: identifiers}
if opts != nil {
if !opts.NotAfter.IsZero() {
orderReq.NotAfter = opts.NotAfter.Format(time.RFC3339)
}
if !opts.NotBefore.IsZero() {
orderReq.NotBefore = opts.NotBefore.Format(time.RFC3339)
}
if o.core.GetDirectory().RenewalInfo != "" {
orderReq.Replaces = opts.ReplacesCertID
}
}
var order acme.Order var order acme.Order
resp, err := o.core.post(o.core.GetDirectory().NewOrderURL, orderReq, &order) resp, err := o.core.post(o.core.GetDirectory().NewOrderURL, orderReq, &order)
if err != nil { if err != nil {
@ -62,24 +25,28 @@ func (o *OrderService) NewWithOptions(domains []string, opts *OrderOptions) (acm
} }
return acme.ExtendedOrder{ return acme.ExtendedOrder{
Order: order, Order: order,
Location: resp.Header.Get("Location"), Location: resp.Header.Get("Location"),
AlternateChainLinks: getLinks(resp.Header, "alternate"),
}, nil }, nil
} }
// Get Gets an order. // Get Gets an order.
func (o *OrderService) Get(orderURL string) (acme.ExtendedOrder, error) { func (o *OrderService) Get(orderURL string) (acme.ExtendedOrder, error) {
if orderURL == "" { if len(orderURL) == 0 {
return acme.ExtendedOrder{}, errors.New("order[get]: empty URL") return acme.ExtendedOrder{}, errors.New("order[get]: empty URL")
} }
var order acme.Order var order acme.Order
_, err := o.core.postAsGet(orderURL, &order) resp, err := o.core.postAsGet(orderURL, &order)
if err != nil { if err != nil {
return acme.ExtendedOrder{}, err return acme.ExtendedOrder{}, err
} }
return acme.ExtendedOrder{Order: order}, nil return acme.ExtendedOrder{
Order: order,
AlternateChainLinks: getLinks(resp.Header, "alternate"),
}, nil
} }
// UpdateForCSR Updates an order for a CSR. // UpdateForCSR Updates an order for a CSR.
@ -89,7 +56,7 @@ func (o *OrderService) UpdateForCSR(orderURL string, csr []byte) (acme.ExtendedO
} }
var order acme.Order var order acme.Order
_, err := o.core.post(orderURL, csrMsg, &order) resp, err := o.core.post(orderURL, csrMsg, &order)
if err != nil { if err != nil {
return acme.ExtendedOrder{}, err return acme.ExtendedOrder{}, err
} }
@ -98,5 +65,8 @@ func (o *OrderService) UpdateForCSR(orderURL string, csr []byte) (acme.ExtendedO
return acme.ExtendedOrder{}, order.Error return acme.ExtendedOrder{}, order.Error
} }
return acme.ExtendedOrder{Order: order}, nil return acme.ExtendedOrder{
Order: order,
AlternateChainLinks: getLinks(resp.Header, "alternate"),
}, nil
} }

View File

@ -1,28 +0,0 @@
package api
import (
"errors"
"net/http"
)
// ErrNoARI is returned when the server does not advertise a renewal info endpoint.
var ErrNoARI = errors.New("renewalInfo[get/post]: server does not advertise a renewal info endpoint")
// GetRenewalInfo GETs renewal information for a certificate from the renewalInfo endpoint.
// This is used to determine if a certificate needs to be renewed.
//
// Note: this endpoint is part of a draft specification, not all ACME servers will implement it.
// This method will return api.ErrNoARI if the server does not advertise a renewal info endpoint.
//
// https://datatracker.ietf.org/doc/draft-ietf-acme-ari
func (c *CertificateService) GetRenewalInfo(certID string) (*http.Response, error) {
if c.core.GetDirectory().RenewalInfo == "" {
return nil, ErrNoARI
}
if certID == "" {
return nil, errors.New("renewalInfo[get]: 'certID' cannot be empty")
}
return c.core.HTTPClient.Get(c.core.GetDirectory().RenewalInfo + "/" + certID)
}

View File

@ -1,5 +1,5 @@
// Package acme contains all objects related the ACME endpoints. // Package acme contains all objects related the ACME endpoints.
// https://www.rfc-editor.org/rfc/rfc8555.html // https://tools.ietf.org/html/rfc8555
package acme package acme
import ( import (
@ -7,38 +7,20 @@ import (
"time" "time"
) )
// ACME status values of Account, Order, Authorization and Challenge objects. // Challenge statuses
// See https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.6 for details. // https://tools.ietf.org/html/rfc8555#section-7.1.6
const ( const (
StatusPending = "pending"
StatusInvalid = "invalid"
StatusValid = "valid"
StatusProcessing = "processing"
StatusDeactivated = "deactivated" StatusDeactivated = "deactivated"
StatusExpired = "expired" StatusExpired = "expired"
StatusInvalid = "invalid"
StatusPending = "pending"
StatusProcessing = "processing"
StatusReady = "ready"
StatusRevoked = "revoked" StatusRevoked = "revoked"
StatusUnknown = "unknown"
StatusValid = "valid"
)
// CRL reason codes as defined in RFC 5280.
// https://datatracker.ietf.org/doc/html/rfc5280#section-5.3.1
const (
CRLReasonUnspecified uint = 0
CRLReasonKeyCompromise uint = 1
CRLReasonCACompromise uint = 2
CRLReasonAffiliationChanged uint = 3
CRLReasonSuperseded uint = 4
CRLReasonCessationOfOperation uint = 5
CRLReasonCertificateHold uint = 6
CRLReasonRemoveFromCRL uint = 8
CRLReasonPrivilegeWithdrawn uint = 9
CRLReasonAACompromise uint = 10
) )
// Directory the ACME directory object. // Directory the ACME directory object.
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.1 // - https://tools.ietf.org/html/rfc8555#section-7.1.1
// - https://datatracker.ietf.org/doc/draft-ietf-acme-ari/
type Directory struct { type Directory struct {
NewNonceURL string `json:"newNonce"` NewNonceURL string `json:"newNonce"`
NewAccountURL string `json:"newAccount"` NewAccountURL string `json:"newAccount"`
@ -47,11 +29,10 @@ type Directory struct {
RevokeCertURL string `json:"revokeCert"` RevokeCertURL string `json:"revokeCert"`
KeyChangeURL string `json:"keyChange"` KeyChangeURL string `json:"keyChange"`
Meta Meta `json:"meta"` Meta Meta `json:"meta"`
RenewalInfo string `json:"renewalInfo"`
} }
// Meta the ACME meta object (related to Directory). // Meta the ACME meta object (related to Directory).
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.1 // - https://tools.ietf.org/html/rfc8555#section-7.1.1
type Meta struct { type Meta struct {
// termsOfService (optional, string): // termsOfService (optional, string):
// A URL identifying the current terms of service. // A URL identifying the current terms of service.
@ -71,12 +52,12 @@ type Meta struct {
// externalAccountRequired (optional, boolean): // externalAccountRequired (optional, boolean):
// If this field is present and set to "true", // If this field is present and set to "true",
// then the CA requires that all new-account requests include an "externalAccountBinding" field // then the CA requires that all new- account requests include an "externalAccountBinding" field
// associating the new account with an external account. // associating the new account with an external account.
ExternalAccountRequired bool `json:"externalAccountRequired"` ExternalAccountRequired bool `json:"externalAccountRequired"`
} }
// ExtendedAccount an extended Account. // ExtendedAccount a extended Account.
type ExtendedAccount struct { type ExtendedAccount struct {
Account Account
// Contains the value of the response header `Location` // Contains the value of the response header `Location`
@ -84,14 +65,14 @@ type ExtendedAccount struct {
} }
// Account the ACME account Object. // Account the ACME account Object.
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.2 // - https://tools.ietf.org/html/rfc8555#section-7.1.2
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.3 // - https://tools.ietf.org/html/rfc8555#section-7.3
type Account struct { type Account struct {
// status (required, string): // status (required, string):
// The status of this account. // The status of this account.
// Possible values are: "valid", "deactivated", and "revoked". // Possible values are: "valid", "deactivated", and "revoked".
// The value "deactivated" should be used to indicate client-initiated deactivation // The value "deactivated" should be used to indicate client-initiated deactivation
// whereas "revoked" should be used to indicate server-initiated deactivation. (See Section 7.1.6) // whereas "revoked" should be used to indicate server- initiated deactivation. (See Section 7.1.6)
Status string `json:"status,omitempty"` Status string `json:"status,omitempty"`
// contact (optional, array of string): // contact (optional, array of string):
@ -125,13 +106,17 @@ type Account struct {
// ExtendedOrder a extended Order. // ExtendedOrder a extended Order.
type ExtendedOrder struct { type ExtendedOrder struct {
Order Order
// The order URL, contains the value of the response header `Location` // The order URL, contains the value of the response header `Location`
Location string `json:"-"` Location string `json:"-"`
// AlternateChainLinks (optional, array of string):
// URLs of "alternate" link relation
// - https://tools.ietf.org/html/rfc8555#section-7.4.2
AlternateChainLinks []string `json:"-"`
} }
// Order the ACME order Object. // Order the ACME order Object.
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.3 // - https://tools.ietf.org/html/rfc8555#section-7.1.3
type Order struct { type Order struct {
// status (required, string): // status (required, string):
// The status of this order. // The status of this order.
@ -181,16 +166,10 @@ type Order struct {
// certificate (optional, string): // certificate (optional, string):
// A URL for the certificate that has been issued in response to this order // A URL for the certificate that has been issued in response to this order
Certificate string `json:"certificate,omitempty"` Certificate string `json:"certificate,omitempty"`
// replaces (optional, string):
// replaces (string, optional): A string uniquely identifying a
// previously-issued certificate which this order is intended to replace.
// - https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5
Replaces string `json:"replaces,omitempty"`
} }
// Authorization the ACME authorization object. // Authorization the ACME authorization object.
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.4 // - https://tools.ietf.org/html/rfc8555#section-7.1.4
type Authorization struct { type Authorization struct {
// status (required, string): // status (required, string):
// The status of this authorization. // The status of this authorization.
@ -232,8 +211,8 @@ type ExtendedChallenge struct {
} }
// Challenge the ACME challenge object. // Challenge the ACME challenge object.
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.5 // - https://tools.ietf.org/html/rfc8555#section-7.1.5
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-8 // - https://tools.ietf.org/html/rfc8555#section-8
type Challenge struct { type Challenge struct {
// type (required, string): // type (required, string):
// The type of challenge encoded in the object. // The type of challenge encoded in the object.
@ -266,23 +245,23 @@ type Challenge struct {
// It MUST NOT contain any characters outside the base64url alphabet, // It MUST NOT contain any characters outside the base64url alphabet,
// and MUST NOT include base64 padding characters ("="). // and MUST NOT include base64 padding characters ("=").
// See [RFC4086] for additional information on randomness requirements. // See [RFC4086] for additional information on randomness requirements.
// https://www.rfc-editor.org/rfc/rfc8555.html#section-8.3 // https://tools.ietf.org/html/rfc8555#section-8.3
// https://www.rfc-editor.org/rfc/rfc8555.html#section-8.4 // https://tools.ietf.org/html/rfc8555#section-8.4
Token string `json:"token"` Token string `json:"token"`
// https://www.rfc-editor.org/rfc/rfc8555.html#section-8.1 // https://tools.ietf.org/html/rfc8555#section-8.1
KeyAuthorization string `json:"keyAuthorization"` KeyAuthorization string `json:"keyAuthorization"`
} }
// Identifier the ACME identifier object. // Identifier the ACME identifier object.
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-9.7.7 // - https://tools.ietf.org/html/rfc8555#section-9.7.7
type Identifier struct { type Identifier struct {
Type string `json:"type"` Type string `json:"type"`
Value string `json:"value"` Value string `json:"value"`
} }
// CSRMessage Certificate Signing Request. // CSRMessage Certificate Signing Request
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4 // - https://tools.ietf.org/html/rfc8555#section-7.4
type CSRMessage struct { type CSRMessage struct {
// csr (required, string): // csr (required, string):
// A CSR encoding the parameters for the certificate being requested [RFC2986]. // A CSR encoding the parameters for the certificate being requested [RFC2986].
@ -291,9 +270,9 @@ type CSRMessage struct {
Csr string `json:"csr"` Csr string `json:"csr"`
} }
// RevokeCertMessage a certificate revocation message. // RevokeCertMessage a certificate revocation message
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.6 // - https://tools.ietf.org/html/rfc8555#section-7.6
// - https://www.rfc-editor.org/rfc/rfc5280.html#section-5.3.1 // - https://tools.ietf.org/html/rfc5280#section-5.3.1
type RevokeCertMessage struct { type RevokeCertMessage struct {
// certificate (required, string): // certificate (required, string):
// The certificate to be revoked, in the base64url-encoded version of the DER format. // The certificate to be revoked, in the base64url-encoded version of the DER format.
@ -308,42 +287,3 @@ type RevokeCertMessage struct {
// The problem document detail SHOULD indicate which reasonCodes are allowed. // The problem document detail SHOULD indicate which reasonCodes are allowed.
Reason *uint `json:"reason,omitempty"` Reason *uint `json:"reason,omitempty"`
} }
// RawCertificate raw data of a certificate.
type RawCertificate struct {
Cert []byte
Issuer []byte
}
// Window is a window of time.
type Window struct {
Start time.Time `json:"start"`
End time.Time `json:"end"`
}
// RenewalInfoResponse is the response to GET requests made the renewalInfo endpoint.
// - (4.1. Getting Renewal Information) https://datatracker.ietf.org/doc/draft-ietf-acme-ari/
type RenewalInfoResponse struct {
// SuggestedWindow contains two fields, start and end,
// whose values are timestamps which bound the window of time in which the CA recommends renewing the certificate.
SuggestedWindow Window `json:"suggestedWindow"`
// ExplanationURL is an optional URL pointing to a page which may explain why the suggested renewal window is what it is.
// For example, it may be a page explaining the CA's dynamic load-balancing strategy,
// or a page documenting which certificates are affected by a mass revocation event.
// Callers SHOULD provide this URL to their operator, if present.
ExplanationURL string `json:"explanationURL"`
}
// RenewalInfoUpdateRequest is the JWS payload for POST requests made to the renewalInfo endpoint.
// - (4.2. RenewalInfo Objects) https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-4.2
type RenewalInfoUpdateRequest struct {
// CertID is a composite string in the format: base64url(AKI) || '.' || base64url(Serial), where AKI is the
// certificate's authority key identifier and Serial is the certificate's serial number. For details, see:
// https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-4.1
CertID string `json:"certID"`
// Replaced is required and indicates whether or not the client considers the certificate to have been replaced.
// A certificate is considered replaced when its revocation would not disrupt any ongoing services,
// for instance because it has been renewed and the new certificate is in use, or because it is no longer in use.
// Clients SHOULD NOT send a request where this value is false.
Replaced bool `json:"replaced"`
}

View File

@ -10,9 +10,9 @@ const (
BadNonceErr = errNS + "badNonce" BadNonceErr = errNS + "badNonce"
) )
// ProblemDetails the problem details object. // ProblemDetails the problem details object
// - https://www.rfc-editor.org/rfc/rfc7807.html#section-3.1 // - https://tools.ietf.org/html/rfc7807#section-3.1
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.3.3 // - https://tools.ietf.org/html/rfc8555#section-7.3.3
type ProblemDetails struct { type ProblemDetails struct {
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
Detail string `json:"detail,omitempty"` Detail string `json:"detail,omitempty"`
@ -25,8 +25,8 @@ type ProblemDetails struct {
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
} }
// SubProblem a "subproblems". // SubProblem a "subproblems"
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-6.7.1 // - https://tools.ietf.org/html/rfc8555#section-6.7.1
type SubProblem struct { type SubProblem struct {
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
Detail string `json:"detail,omitempty"` Detail string `json:"detail,omitempty"`
@ -35,7 +35,7 @@ type SubProblem struct {
func (p ProblemDetails) Error() string { func (p ProblemDetails) Error() string {
msg := fmt.Sprintf("acme: error: %d", p.HTTPStatus) msg := fmt.Sprintf("acme: error: %d", p.HTTPStatus)
if p.Method != "" || p.URL != "" { if len(p.Method) != 0 || len(p.URL) != 0 {
msg += fmt.Sprintf(" :: %s :: %s", p.Method, p.URL) msg += fmt.Sprintf(" :: %s :: %s", p.Method, p.URL)
} }
msg += fmt.Sprintf(" :: %s :: %s", p.Type, p.Detail) msg += fmt.Sprintf(" :: %s :: %s", p.Type, p.Detail)
@ -44,7 +44,7 @@ func (p ProblemDetails) Error() string {
msg += fmt.Sprintf(", problem: %q :: %s", sub.Type, sub.Detail) msg += fmt.Sprintf(", problem: %q :: %s", sub.Type, sub.Detail)
} }
if p.Instance != "" { if len(p.Instance) == 0 {
msg += ", url: " + p.Instance msg += ", url: " + p.Instance
} }

View File

@ -14,8 +14,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"math/big" "math/big"
"net"
"slices"
"strings" "strings"
"time" "time"
@ -27,7 +25,6 @@ const (
EC256 = KeyType("P256") EC256 = KeyType("P256")
EC384 = KeyType("P384") EC384 = KeyType("P384")
RSA2048 = KeyType("2048") RSA2048 = KeyType("2048")
RSA3072 = KeyType("3072")
RSA4096 = KeyType("4096") RSA4096 = KeyType("4096")
RSA8192 = KeyType("8192") RSA8192 = KeyType("8192")
) )
@ -85,12 +82,9 @@ func ParsePEMBundle(bundle []byte) ([]*x509.Certificate, error) {
// ParsePEMPrivateKey parses a private key from key, which is a PEM block. // ParsePEMPrivateKey parses a private key from key, which is a PEM block.
// Borrowed from Go standard library, to handle various private key and PEM block types. // Borrowed from Go standard library, to handle various private key and PEM block types.
// https://github.com/golang/go/blob/693748e9fa385f1e2c3b91ca9acbb6c0ad2d133d/src/crypto/tls/tls.go#L291-L308 // https://github.com/golang/go/blob/693748e9fa385f1e2c3b91ca9acbb6c0ad2d133d/src/crypto/tls/tls.go#L291-L308
// https://github.com/golang/go/blob/693748e9fa385f1e2c3b91ca9acbb6c0ad2d133d/src/crypto/tls/tls.go#L238 // https://github.com/golang/go/blob/693748e9fa385f1e2c3b91ca9acbb6c0ad2d133d/src/crypto/tls/tls.go#L238)
func ParsePEMPrivateKey(key []byte) (crypto.PrivateKey, error) { func ParsePEMPrivateKey(key []byte) (crypto.PrivateKey, error) {
keyBlockDER, _ := pem.Decode(key) keyBlockDER, _ := pem.Decode(key)
if keyBlockDER == nil {
return nil, errors.New("invalid PEM block")
}
if keyBlockDER.Type != "PRIVATE KEY" && !strings.HasSuffix(keyBlockDER.Type, " PRIVATE KEY") { if keyBlockDER.Type != "PRIVATE KEY" && !strings.HasSuffix(keyBlockDER.Type, " PRIVATE KEY") {
return nil, fmt.Errorf("unknown PEM header %q", keyBlockDER.Type) return nil, fmt.Errorf("unknown PEM header %q", keyBlockDER.Type)
@ -124,8 +118,6 @@ func GeneratePrivateKey(keyType KeyType) (crypto.PrivateKey, error) {
return ecdsa.GenerateKey(elliptic.P384(), rand.Reader) return ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
case RSA2048: case RSA2048:
return rsa.GenerateKey(rand.Reader, 2048) return rsa.GenerateKey(rand.Reader, 2048)
case RSA3072:
return rsa.GenerateKey(rand.Reader, 3072)
case RSA4096: case RSA4096:
return rsa.GenerateKey(rand.Reader, 4096) return rsa.GenerateKey(rand.Reader, 4096)
case RSA8192: case RSA8192:
@ -136,20 +128,9 @@ func GeneratePrivateKey(keyType KeyType) (crypto.PrivateKey, error) {
} }
func GenerateCSR(privateKey crypto.PrivateKey, domain string, san []string, mustStaple bool) ([]byte, error) { func GenerateCSR(privateKey crypto.PrivateKey, domain string, san []string, mustStaple bool) ([]byte, error) {
var dnsNames []string
var ipAddresses []net.IP
for _, altname := range san {
if ip := net.ParseIP(altname); ip != nil {
ipAddresses = append(ipAddresses, ip)
} else {
dnsNames = append(dnsNames, altname)
}
}
template := x509.CertificateRequest{ template := x509.CertificateRequest{
Subject: pkix.Name{CommonName: domain}, Subject: pkix.Name{CommonName: domain},
DNSNames: dnsNames, DNSNames: san,
IPAddresses: ipAddresses,
} }
if mustStaple { if mustStaple {
@ -192,13 +173,13 @@ func pemDecode(data []byte) (*pem.Block, error) {
return pemBlock, nil return pemBlock, nil
} }
func PemDecodeTox509CSR(data []byte) (*x509.CertificateRequest, error) { func PemDecodeTox509CSR(pem []byte) (*x509.CertificateRequest, error) {
pemBlock, err := pemDecode(data) pemBlock, err := pemDecode(pem)
if pemBlock == nil { if pemBlock == nil {
return nil, err return nil, err
} }
if pemBlock.Type != "CERTIFICATE REQUEST" && pemBlock.Type != "NEW CERTIFICATE REQUEST" { if pemBlock.Type != "CERTIFICATE REQUEST" {
return nil, errors.New("PEM block is not a certificate request") return nil, errors.New("PEM block is not a certificate request")
} }
@ -217,26 +198,6 @@ func ParsePEMCertificate(cert []byte) (*x509.Certificate, error) {
return x509.ParseCertificate(pemBlock.Bytes) return x509.ParseCertificate(pemBlock.Bytes)
} }
func GetCertificateMainDomain(cert *x509.Certificate) (string, error) {
return getMainDomain(cert.Subject, cert.DNSNames)
}
func GetCSRMainDomain(cert *x509.CertificateRequest) (string, error) {
return getMainDomain(cert.Subject, cert.DNSNames)
}
func getMainDomain(subject pkix.Name, dnsNames []string) (string, error) {
if subject.CommonName == "" && len(dnsNames) == 0 {
return "", errors.New("missing domain")
}
if subject.CommonName != "" {
return subject.CommonName, nil
}
return dnsNames[0], nil
}
func ExtractDomains(cert *x509.Certificate) []string { func ExtractDomains(cert *x509.Certificate) []string {
var domains []string var domains []string
if cert.Subject.CommonName != "" { if cert.Subject.CommonName != "" {
@ -251,13 +212,6 @@ func ExtractDomains(cert *x509.Certificate) []string {
domains = append(domains, sanDomain) domains = append(domains, sanDomain)
} }
commonNameIP := net.ParseIP(cert.Subject.CommonName)
for _, sanIP := range cert.IPAddresses {
if !commonNameIP.Equal(sanIP) {
domains = append(domains, sanIP.String())
}
}
return domains return domains
} }
@ -269,7 +223,7 @@ func ExtractDomainsCSR(csr *x509.CertificateRequest) []string {
// loop over the SubjectAltName DNS names // loop over the SubjectAltName DNS names
for _, sanName := range csr.DNSNames { for _, sanName := range csr.DNSNames {
if slices.Contains(domains, sanName) { if containsSAN(domains, sanName) {
// Duplicate; skip this name // Duplicate; skip this name
continue continue
} }
@ -278,14 +232,16 @@ func ExtractDomainsCSR(csr *x509.CertificateRequest) []string {
domains = append(domains, sanName) domains = append(domains, sanName)
} }
cnip := net.ParseIP(csr.Subject.CommonName) return domains
for _, sanIP := range csr.IPAddresses { }
if !cnip.Equal(sanIP) {
domains = append(domains, sanIP.String()) func containsSAN(domains []string, sanName string) bool {
for _, existingName := range domains {
if existingName == sanName {
return true
} }
} }
return false
return domains
} }
func GeneratePemCert(privateKey *rsa.PrivateKey, domain string, extensions []pkix.Extension) ([]byte, error) { func GeneratePemCert(privateKey *rsa.PrivateKey, domain string, extensions []pkix.Extension) ([]byte, error) {
@ -305,7 +261,7 @@ func generateDerCert(privateKey *rsa.PrivateKey, expiration time.Time, domain st
} }
if expiration.IsZero() { if expiration.IsZero() {
expiration = time.Now().AddDate(1, 0, 0) expiration = time.Now().Add(365)
} }
template := x509.Certificate{ template := x509.Certificate{
@ -318,15 +274,9 @@ func generateDerCert(privateKey *rsa.PrivateKey, expiration time.Time, domain st
KeyUsage: x509.KeyUsageKeyEncipherment, KeyUsage: x509.KeyUsageKeyEncipherment,
BasicConstraintsValid: true, BasicConstraintsValid: true,
DNSNames: []string{domain},
ExtraExtensions: extensions, ExtraExtensions: extensions,
} }
// handling SAN filling as type suspected
if ip := net.ParseIP(domain); ip != nil {
template.IPAddresses = []net.IP{ip}
} else {
template.DNSNames = []string{domain}
}
return x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) return x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
} }

View File

@ -12,7 +12,6 @@ const (
// limited on the "new-reg", "new-authz" and "new-cert" endpoints. // limited on the "new-reg", "new-authz" and "new-cert" endpoints.
// From the documentation the limitation is 20 requests per second, // From the documentation the limitation is 20 requests per second,
// but using 20 as value doesn't work but 18 do. // but using 20 as value doesn't work but 18 do.
// https://letsencrypt.org/docs/rate-limits/
overallRequestLimit = 18 overallRequestLimit = 18
) )
@ -36,14 +35,13 @@ func (c *Certifier) getAuthorizations(order acme.ExtendedOrder) ([]acme.Authoriz
} }
var responses []acme.Authorization var responses []acme.Authorization
failures := make(obtainError)
failures := newObtainError() for i := 0; i < len(order.Authorizations); i++ {
for range len(order.Authorizations) {
select { select {
case res := <-resc: case res := <-resc:
responses = append(responses, res) responses = append(responses, res)
case err := <-errc: case err := <-errc:
failures.Add(err.Domain, err.Error) failures[err.Domain] = err.Error
} }
} }
@ -54,10 +52,15 @@ func (c *Certifier) getAuthorizations(order acme.ExtendedOrder) ([]acme.Authoriz
close(resc) close(resc)
close(errc) close(errc)
return responses, failures.Join() // be careful to not return an empty failures map;
// even if empty, they become non-nil error values
if len(failures) > 0 {
return responses, failures
}
return responses, nil
} }
func (c *Certifier) deactivateAuthorizations(order acme.ExtendedOrder, force bool) { func (c *Certifier) deactivateAuthorizations(order acme.ExtendedOrder) {
for _, authzURL := range order.Authorizations { for _, authzURL := range order.Authorizations {
auth, err := c.core.Authorizations.Get(authzURL) auth, err := c.core.Authorizations.Get(authzURL)
if err != nil { if err != nil {
@ -65,7 +68,7 @@ func (c *Certifier) deactivateAuthorizations(order acme.ExtendedOrder, force boo
continue continue
} }
if auth.Status == acme.StatusValid && !force { if auth.Status == acme.StatusValid {
log.Infof("Skipping deactivating of valid auth: %s", authzURL) log.Infof("Skipping deactivating of valid auth: %s", authzURL)
continue continue
} }

View File

@ -7,7 +7,7 @@ import (
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
"io" "io/ioutil"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@ -49,44 +49,22 @@ type Resource struct {
// If you do not want that you can supply your own private key in the privateKey parameter. // If you do not want that you can supply your own private key in the privateKey parameter.
// If this parameter is non-nil it will be used instead of generating a new one. // If this parameter is non-nil it will be used instead of generating a new one.
// //
// If `Bundle` is true, the `[]byte` contains both the issuer certificate and your issued certificate as a bundle. // If bundle is true, the []byte contains both the issuer certificate and your issued certificate as a bundle.
//
// If `AlwaysDeactivateAuthorizations` is true, the authorizations are also relinquished if the obtain request was successful.
// See https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.2.
type ObtainRequest struct { type ObtainRequest struct {
Domains []string Domains []string
PrivateKey crypto.PrivateKey Bundle bool
MustStaple bool PrivateKey crypto.PrivateKey
MustStaple bool
NotBefore time.Time PreferredChain string
NotAfter time.Time
Bundle bool
PreferredChain string
AlwaysDeactivateAuthorizations bool
// A string uniquely identifying a previously-issued certificate which this
// order is intended to replace.
// - https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5
ReplacesCertID string
} }
// ObtainForCSRRequest The request to obtain a certificate matching the CSR passed into it. // ObtainForCSRRequest The request to obtain a certificate matching the CSR passed into it.
// //
// If `Bundle` is true, the `[]byte` contains both the issuer certificate and your issued certificate as a bundle. // If bundle is true, the []byte contains both the issuer certificate and your issued certificate as a bundle.
//
// If `AlwaysDeactivateAuthorizations` is true, the authorizations are also relinquished if the obtain request was successful.
// See https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.2.
type ObtainForCSRRequest struct { type ObtainForCSRRequest struct {
CSR *x509.CertificateRequest CSR *x509.CertificateRequest
Bundle bool
NotBefore time.Time PreferredChain string
NotAfter time.Time
Bundle bool
PreferredChain string
AlwaysDeactivateAuthorizations bool
// A string uniquely identifying a previously-issued certificate which this
// order is intended to replace.
// - https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5
ReplacesCertID string
} }
type resolver interface { type resolver interface {
@ -131,13 +109,7 @@ func (c *Certifier) Obtain(request ObtainRequest) (*Resource, error) {
log.Infof("[%s] acme: Obtaining SAN certificate", strings.Join(domains, ", ")) log.Infof("[%s] acme: Obtaining SAN certificate", strings.Join(domains, ", "))
} }
orderOpts := &api.OrderOptions{ order, err := c.core.Orders.New(domains)
NotBefore: request.NotBefore,
NotAfter: request.NotAfter,
ReplacesCertID: request.ReplacesCertID,
}
order, err := c.core.Orders.NewWithOptions(domains, orderOpts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -145,32 +117,33 @@ func (c *Certifier) Obtain(request ObtainRequest) (*Resource, error) {
authz, err := c.getAuthorizations(order) authz, err := c.getAuthorizations(order)
if err != nil { if err != nil {
// If any challenge fails, return. Do not generate partial SAN certificates. // If any challenge fails, return. Do not generate partial SAN certificates.
c.deactivateAuthorizations(order, request.AlwaysDeactivateAuthorizations) c.deactivateAuthorizations(order)
return nil, err return nil, err
} }
err = c.resolver.Solve(authz) err = c.resolver.Solve(authz)
if err != nil { if err != nil {
// If any challenge fails, return. Do not generate partial SAN certificates. // If any challenge fails, return. Do not generate partial SAN certificates.
c.deactivateAuthorizations(order, request.AlwaysDeactivateAuthorizations) c.deactivateAuthorizations(order)
return nil, err return nil, err
} }
log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
failures := newObtainError() failures := make(obtainError)
cert, err := c.getForOrder(domains, order, request.Bundle, request.PrivateKey, request.MustStaple, request.PreferredChain) cert, err := c.getForOrder(domains, order, request.Bundle, request.PrivateKey, request.MustStaple, request.PreferredChain)
if err != nil { if err != nil {
for _, auth := range authz { for _, auth := range authz {
failures.Add(challenge.GetTargetedDomain(auth), err) failures[challenge.GetTargetedDomain(auth)] = err
} }
} }
if request.AlwaysDeactivateAuthorizations { // Do not return an empty failures map, because
c.deactivateAuthorizations(order, true) // it would still be a non-nil error value
if len(failures) > 0 {
return cert, failures
} }
return cert, nil
return cert, failures.Join()
} }
// ObtainForCSR tries to obtain a certificate matching the CSR passed into it. // ObtainForCSR tries to obtain a certificate matching the CSR passed into it.
@ -197,13 +170,7 @@ func (c *Certifier) ObtainForCSR(request ObtainForCSRRequest) (*Resource, error)
log.Infof("[%s] acme: Obtaining SAN certificate given a CSR", strings.Join(domains, ", ")) log.Infof("[%s] acme: Obtaining SAN certificate given a CSR", strings.Join(domains, ", "))
} }
orderOpts := &api.OrderOptions{ order, err := c.core.Orders.New(domains)
NotBefore: request.NotBefore,
NotAfter: request.NotAfter,
ReplacesCertID: request.ReplacesCertID,
}
order, err := c.core.Orders.NewWithOptions(domains, orderOpts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -211,37 +178,38 @@ func (c *Certifier) ObtainForCSR(request ObtainForCSRRequest) (*Resource, error)
authz, err := c.getAuthorizations(order) authz, err := c.getAuthorizations(order)
if err != nil { if err != nil {
// If any challenge fails, return. Do not generate partial SAN certificates. // If any challenge fails, return. Do not generate partial SAN certificates.
c.deactivateAuthorizations(order, request.AlwaysDeactivateAuthorizations) c.deactivateAuthorizations(order)
return nil, err return nil, err
} }
err = c.resolver.Solve(authz) err = c.resolver.Solve(authz)
if err != nil { if err != nil {
// If any challenge fails, return. Do not generate partial SAN certificates. // If any challenge fails, return. Do not generate partial SAN certificates.
c.deactivateAuthorizations(order, request.AlwaysDeactivateAuthorizations) c.deactivateAuthorizations(order)
return nil, err return nil, err
} }
log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
failures := newObtainError() failures := make(obtainError)
cert, err := c.getForCSR(domains, order, request.Bundle, request.CSR.Raw, nil, request.PreferredChain) cert, err := c.getForCSR(domains, order, request.Bundle, request.CSR.Raw, nil, request.PreferredChain)
if err != nil { if err != nil {
for _, auth := range authz { for _, auth := range authz {
failures.Add(challenge.GetTargetedDomain(auth), err) failures[challenge.GetTargetedDomain(auth)] = err
} }
} }
if request.AlwaysDeactivateAuthorizations {
c.deactivateAuthorizations(order, true)
}
if cert != nil { if cert != nil {
// Add the CSR to the certificate so that it can be used for renewals. // Add the CSR to the certificate so that it can be used for renewals.
cert.CSR = certcrypto.PEMEncode(request.CSR) cert.CSR = certcrypto.PEMEncode(request.CSR)
} }
return cert, failures.Join() // Do not return an empty failures map,
// because it would still be a non-nil error value
if len(failures) > 0 {
return cert, failures
}
return cert, nil
} }
func (c *Certifier) getForOrder(domains []string, order acme.ExtendedOrder, bundle bool, privateKey crypto.PrivateKey, mustStaple bool, preferredChain string) (*Resource, error) { func (c *Certifier) getForOrder(domains []string, order acme.ExtendedOrder, bundle bool, privateKey crypto.PrivateKey, mustStaple bool, preferredChain string) (*Resource, error) {
@ -253,23 +221,16 @@ func (c *Certifier) getForOrder(domains []string, order acme.ExtendedOrder, bund
} }
} }
commonName := "" // Determine certificate name(s) based on the authorization resources
if len(domains[0]) <= 64 { commonName := domains[0]
commonName = domains[0]
}
// RFC8555 Section 7.4 "Applying for Certificate Issuance" // RFC8555 Section 7.4 "Applying for Certificate Issuance"
// https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4 // https://tools.ietf.org/html/rfc8555#section-7.4
// says: // says:
// Clients SHOULD NOT make any assumptions about the sort order of // Clients SHOULD NOT make any assumptions about the sort order of
// "identifiers" or "authorizations" elements in the returned order // "identifiers" or "authorizations" elements in the returned order
// object. // object.
san := []string{commonName}
var san []string
if commonName != "" {
san = append(san, commonName)
}
for _, auth := range order.Identifiers { for _, auth := range order.Identifiers {
if auth.Value != commonName { if auth.Value != commonName {
san = append(san, auth.Value) san = append(san, auth.Value)
@ -291,14 +252,15 @@ func (c *Certifier) getForCSR(domains []string, order acme.ExtendedOrder, bundle
return nil, err return nil, err
} }
commonName := domains[0]
certRes := &Resource{ certRes := &Resource{
Domain: domains[0], Domain: commonName,
CertURL: respOrder.Certificate, CertURL: respOrder.Certificate,
PrivateKey: privateKeyPem, PrivateKey: privateKeyPem,
} }
if respOrder.Status == acme.StatusValid { if respOrder.Status == acme.StatusValid {
// if the certificate is available right away, shortcut! // if the certificate is available right away, short cut!
ok, errR := c.checkResponse(respOrder, certRes, bundle, preferredChain) ok, errR := c.checkResponse(respOrder, certRes, bundle, preferredChain)
if errR != nil { if errR != nil {
return nil, errR return nil, errR
@ -345,25 +307,29 @@ func (c *Certifier) checkResponse(order acme.ExtendedOrder, certRes *Resource, b
return valid, err return valid, err
} }
certs, err := c.core.Certificates.GetAll(order.Certificate, bundle) links := append([]string{order.Certificate}, order.AlternateChainLinks...)
if err != nil {
return false, err
}
// Set the default certificate for i, link := range links {
certRes.IssuerCertificate = certs[order.Certificate].Issuer cert, issuer, err := c.core.Certificates.Get(link, bundle)
certRes.Certificate = certs[order.Certificate].Cert if err != nil {
certRes.CertURL = order.Certificate return false, err
certRes.CertStableURL = order.Certificate }
if preferredChain == "" { // Set the default certificate
log.Infof("[%s] Server responded with a certificate.", certRes.Domain) if i == 0 {
certRes.IssuerCertificate = issuer
certRes.Certificate = cert
certRes.CertURL = link
certRes.CertStableURL = link
}
return true, nil if preferredChain == "" {
} log.Infof("[%s] Server responded with a certificate.", certRes.Domain)
for link, cert := range certs { return true, nil
ok, err := hasPreferredChain(cert.Issuer, preferredChain) }
ok, err := hasPreferredChain(issuer, preferredChain)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -371,8 +337,8 @@ func (c *Certifier) checkResponse(order acme.ExtendedOrder, certRes *Resource, b
if ok { if ok {
log.Infof("[%s] Server responded with a certificate for the preferred certificate chains %q.", certRes.Domain, preferredChain) log.Infof("[%s] Server responded with a certificate for the preferred certificate chains %q.", certRes.Domain, preferredChain)
certRes.IssuerCertificate = cert.Issuer certRes.IssuerCertificate = issuer
certRes.Certificate = cert.Cert certRes.Certificate = cert
certRes.CertURL = link certRes.CertURL = link
certRes.CertStableURL = link certRes.CertStableURL = link
@ -387,11 +353,6 @@ func (c *Certifier) checkResponse(order acme.ExtendedOrder, certRes *Resource, b
// Revoke takes a PEM encoded certificate or bundle and tries to revoke it at the CA. // Revoke takes a PEM encoded certificate or bundle and tries to revoke it at the CA.
func (c *Certifier) Revoke(cert []byte) error { func (c *Certifier) Revoke(cert []byte) error {
return c.RevokeWithReason(cert, nil)
}
// RevokeWithReason takes a PEM encoded certificate or bundle and tries to revoke it at the CA.
func (c *Certifier) RevokeWithReason(cert []byte, reason *uint) error {
certificates, err := certcrypto.ParsePEMBundle(cert) certificates, err := certcrypto.ParsePEMBundle(cert)
if err != nil { if err != nil {
return err return err
@ -404,24 +365,11 @@ func (c *Certifier) RevokeWithReason(cert []byte, reason *uint) error {
revokeMsg := acme.RevokeCertMessage{ revokeMsg := acme.RevokeCertMessage{
Certificate: base64.RawURLEncoding.EncodeToString(x509Cert.Raw), Certificate: base64.RawURLEncoding.EncodeToString(x509Cert.Raw),
Reason: reason,
} }
return c.core.Certificates.Revoke(revokeMsg) return c.core.Certificates.Revoke(revokeMsg)
} }
// RenewOptions options used by Certifier.RenewWithOptions.
type RenewOptions struct {
NotBefore time.Time
NotAfter time.Time
// If true, the []byte contains both the issuer certificate and your issued certificate as a bundle.
Bundle bool
PreferredChain string
AlwaysDeactivateAuthorizations bool
// Not supported for CSR request.
MustStaple bool
}
// Renew takes a Resource and tries to renew the certificate. // Renew takes a Resource and tries to renew the certificate.
// //
// If the renewal process succeeds, the new certificate will be returned in a new CertResource. // If the renewal process succeeds, the new certificate will be returned in a new CertResource.
@ -432,26 +380,7 @@ type RenewOptions struct {
// If bundle is true, the []byte contains both the issuer certificate and your issued certificate as a bundle. // If bundle is true, the []byte contains both the issuer certificate and your issued certificate as a bundle.
// //
// For private key reuse the PrivateKey property of the passed in Resource should be non-nil. // For private key reuse the PrivateKey property of the passed in Resource should be non-nil.
// Deprecated: use RenewWithOptions instead.
func (c *Certifier) Renew(certRes Resource, bundle, mustStaple bool, preferredChain string) (*Resource, error) { func (c *Certifier) Renew(certRes Resource, bundle, mustStaple bool, preferredChain string) (*Resource, error) {
return c.RenewWithOptions(certRes, &RenewOptions{
Bundle: bundle,
PreferredChain: preferredChain,
MustStaple: mustStaple,
})
}
// RenewWithOptions takes a Resource and tries to renew the certificate.
//
// If the renewal process succeeds, the new certificate will be returned in a new CertResource.
// Please be aware that this function will return a new certificate in ANY case that is not an error.
// If the server does not provide us with a new cert on a GET request to the CertURL
// this function will start a new-cert flow where a new certificate gets generated.
//
// If bundle is true, the []byte contains both the issuer certificate and your issued certificate as a bundle.
//
// For private key reuse the PrivateKey property of the passed in Resource should be non-nil.
func (c *Certifier) RenewWithOptions(certRes Resource, options *RenewOptions) (*Resource, error) {
// Input certificate is PEM encoded. // Input certificate is PEM encoded.
// Decode it here as we may need the decoded cert later on in the renewal process. // Decode it here as we may need the decoded cert later on in the renewal process.
// The input may be a bundle or a single certificate. // The input may be a bundle or a single certificate.
@ -478,17 +407,11 @@ func (c *Certifier) RenewWithOptions(certRes Resource, options *RenewOptions) (*
return nil, errP return nil, errP
} }
request := ObtainForCSRRequest{CSR: csr} return c.ObtainForCSR(ObtainForCSRRequest{
CSR: csr,
if options != nil { Bundle: bundle,
request.NotBefore = options.NotBefore PreferredChain: preferredChain,
request.NotAfter = options.NotAfter })
request.Bundle = options.Bundle
request.PreferredChain = options.PreferredChain
request.AlwaysDeactivateAuthorizations = options.AlwaysDeactivateAuthorizations
}
return c.ObtainForCSR(request)
} }
var privateKey crypto.PrivateKey var privateKey crypto.PrivateKey
@ -499,21 +422,13 @@ func (c *Certifier) RenewWithOptions(certRes Resource, options *RenewOptions) (*
} }
} }
request := ObtainRequest{ query := ObtainRequest{
Domains: certcrypto.ExtractDomains(x509Cert), Domains: certcrypto.ExtractDomains(x509Cert),
Bundle: bundle,
PrivateKey: privateKey, PrivateKey: privateKey,
MustStaple: mustStaple,
} }
return c.Obtain(query)
if options != nil {
request.MustStaple = options.MustStaple
request.NotBefore = options.NotBefore
request.NotAfter = options.NotAfter
request.Bundle = options.Bundle
request.PreferredChain = options.PreferredChain
request.AlwaysDeactivateAuthorizations = options.AlwaysDeactivateAuthorizations
}
return c.Obtain(request)
} }
// GetOCSP takes a PEM encoded cert or cert bundle returning the raw OCSP response, // GetOCSP takes a PEM encoded cert or cert bundle returning the raw OCSP response,
@ -554,7 +469,7 @@ func (c *Certifier) GetOCSP(bundle []byte) ([]byte, *ocsp.Response, error) {
} }
defer resp.Body.Close() defer resp.Body.Close()
issuerBytes, errC := io.ReadAll(http.MaxBytesReader(nil, resp.Body, maxBodySize)) issuerBytes, errC := ioutil.ReadAll(http.MaxBytesReader(nil, resp.Body, maxBodySize))
if errC != nil { if errC != nil {
return nil, nil, errC return nil, nil, errC
} }
@ -583,7 +498,7 @@ func (c *Certifier) GetOCSP(bundle []byte) ([]byte, *ocsp.Response, error) {
} }
defer resp.Body.Close() defer resp.Body.Close()
ocspResBytes, err := io.ReadAll(http.MaxBytesReader(nil, resp.Body, maxBodySize)) ocspResBytes, err := ioutil.ReadAll(http.MaxBytesReader(nil, resp.Body, maxBodySize))
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -614,13 +529,8 @@ func (c *Certifier) Get(url string, bundle bool) (*Resource, error) {
return nil, err return nil, err
} }
domain, err := certcrypto.GetCertificateMainDomain(x509Certs[0])
if err != nil {
return nil, err
}
return &Resource{ return &Resource{
Domain: domain, Domain: x509Certs[0].Subject.CommonName,
Certificate: cert, Certificate: cert,
IssuerCertificate: issuer, IssuerCertificate: issuer,
CertURL: url, CertURL: url,
@ -634,10 +544,10 @@ func hasPreferredChain(issuer []byte, preferredChain string) (bool, error) {
return false, err return false, err
} }
topCert := certs[len(certs)-1] for _, cert := range certs {
if cert.Issuer.CommonName == preferredChain {
if topCert.Issuer.CommonName == preferredChain { return true, nil
return true, nil }
} }
return false, nil return false, nil
@ -654,11 +564,12 @@ func checkOrderStatus(order acme.ExtendedOrder) (bool, error) {
} }
} }
// https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.4 // https://tools.ietf.org/html/rfc8555#section-7.1.4
// The domain name MUST be encoded in the form in which it would appear in a certificate. // The domain name MUST be encoded
// That is, it MUST be encoded according to the rules in Section 7 of [RFC5280]. // in the form in which it would appear in a certificate. That is, it
// MUST be encoded according to the rules in Section 7 of [RFC5280].
// //
// https://www.rfc-editor.org/rfc/rfc5280.html#section-7 // https://tools.ietf.org/html/rfc5280#section-7
func sanitizeDomain(domains []string) []string { func sanitizeDomain(domains []string) []string {
var sanitizedDomains []string var sanitizedDomains []string
for _, domain := range domains { for _, domain := range domains {

View File

@ -1,37 +1,27 @@
package certificate package certificate
import ( import (
"errors" "bytes"
"fmt" "fmt"
"sort"
) )
type obtainError struct { // obtainError is returned when there are specific errors available per domain.
data map[string]error type obtainError map[string]error
}
func newObtainError() *obtainError { func (e obtainError) Error() string {
return &obtainError{data: make(map[string]error)} buffer := bytes.NewBufferString("error: one or more domains had a problem:\n")
}
func (e *obtainError) Add(domain string, err error) { var domains []string
e.data[domain] = err for domain := range e {
} domains = append(domains, domain)
func (e *obtainError) Join() error {
if e == nil {
return nil
} }
sort.Strings(domains)
if len(e.data) == 0 { for _, domain := range domains {
return nil buffer.WriteString(fmt.Sprintf("[%s] %s\n", domain, e[domain]))
} }
return buffer.String()
var err error
for d, e := range e.data {
err = errors.Join(err, fmt.Errorf("%s: %w", d, e))
}
return fmt.Errorf("error: one or more domains had a problem:\n%w", err)
} }
type domainError struct { type domainError struct {

View File

@ -1,129 +0,0 @@
package certificate
import (
"crypto/x509"
"encoding/asn1"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"math/rand"
"time"
"github.com/go-acme/lego/v4/acme"
)
// RenewalInfoRequest contains the necessary renewal information.
type RenewalInfoRequest struct {
Cert *x509.Certificate
}
// RenewalInfoResponse is a wrapper around acme.RenewalInfoResponse that provides a method for determining when to renew a certificate.
type RenewalInfoResponse struct {
acme.RenewalInfoResponse
// RetryAfter header indicating the polling interval that the ACME server recommends.
// Conforming clients SHOULD query the renewalInfo URL again after the RetryAfter period has passed,
// as the server may provide a different suggestedWindow.
// https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-4.2
RetryAfter time.Duration
}
// ShouldRenewAt determines the optimal renewal time based on the current time (UTC),renewal window suggest by ARI, and the client's willingness to sleep.
// It returns a pointer to a time.Time value indicating when the renewal should be attempted or nil if deferred until the next normal wake time.
// This method implements the RECOMMENDED algorithm described in draft-ietf-acme-ari.
//
// - (4.1-11. Getting Renewal Information) https://datatracker.ietf.org/doc/draft-ietf-acme-ari/
func (r *RenewalInfoResponse) ShouldRenewAt(now time.Time, willingToSleep time.Duration) *time.Time {
// Explicitly convert all times to UTC.
now = now.UTC()
start := r.SuggestedWindow.Start.UTC()
end := r.SuggestedWindow.End.UTC()
// Select a uniform random time within the suggested window.
window := end.Sub(start)
randomDuration := time.Duration(rand.Int63n(int64(window)))
rt := start.Add(randomDuration)
// If the selected time is in the past, attempt renewal immediately.
if rt.Before(now) {
return &now
}
// Otherwise, if the client can schedule itself to attempt renewal at exactly the selected time, do so.
willingToSleepUntil := now.Add(willingToSleep)
if willingToSleepUntil.After(rt) || willingToSleepUntil.Equal(rt) {
return &rt
}
// TODO: Otherwise, if the selected time is before the next time that the client would wake up normally, attempt renewal immediately.
// Otherwise, sleep until the next normal wake time, re-check ARI, and return to Step 1.
return nil
}
// GetRenewalInfo sends a request to the ACME server's renewalInfo endpoint to obtain a suggested renewal window.
// The caller MUST provide the certificate and issuer certificate for the certificate they wish to renew.
// The caller should attempt to renew the certificate at the time indicated by the ShouldRenewAt method of the returned RenewalInfoResponse object.
//
// Note: this endpoint is part of a draft specification, not all ACME servers will implement it.
// This method will return api.ErrNoARI if the server does not advertise a renewal info endpoint.
//
// https://datatracker.ietf.org/doc/draft-ietf-acme-ari
func (c *Certifier) GetRenewalInfo(req RenewalInfoRequest) (*RenewalInfoResponse, error) {
certID, err := MakeARICertID(req.Cert)
if err != nil {
return nil, fmt.Errorf("error making certID: %w", err)
}
resp, err := c.core.Certificates.GetRenewalInfo(certID)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var info RenewalInfoResponse
err = json.NewDecoder(resp.Body).Decode(&info)
if err != nil {
return nil, err
}
if retry := resp.Header.Get("Retry-After"); retry != "" {
info.RetryAfter, err = time.ParseDuration(retry + "s")
if err != nil {
return nil, err
}
}
return &info, nil
}
// MakeARICertID constructs a certificate identifier as described in draft-ietf-acme-ari-03, section 4.1.
func MakeARICertID(leaf *x509.Certificate) (string, error) {
if leaf == nil {
return "", errors.New("leaf certificate is nil")
}
// Marshal the Serial Number into DER.
der, err := asn1.Marshal(leaf.SerialNumber)
if err != nil {
return "", err
}
// Check if the DER encoded bytes are sufficient (at least 3 bytes: tag,
// length, and value).
if len(der) < 3 {
return "", errors.New("invalid DER encoding of serial number")
}
// Extract only the integer bytes from the DER encoded Serial Number
// Skipping the first 2 bytes (tag and length).
serial := base64.RawURLEncoding.EncodeToString(der[2:])
// Convert the Authority Key Identifier to base64url encoding without
// padding.
aki := base64.RawURLEncoding.EncodeToString(leaf.AuthorityKeyId)
// Construct the final identifier by concatenating AKI and Serial Number.
return fmt.Sprintf("%s.%s", aki, serial), nil
}

View File

@ -10,15 +10,15 @@ import (
type Type string type Type string
const ( const (
// HTTP01 is the "http-01" ACME challenge https://www.rfc-editor.org/rfc/rfc8555.html#section-8.3 // HTTP01 is the "http-01" ACME challenge https://tools.ietf.org/html/rfc8555#section-8.3
// Note: ChallengePath returns the URL path to fulfill this challenge. // Note: ChallengePath returns the URL path to fulfill this challenge.
HTTP01 = Type("http-01") HTTP01 = Type("http-01")
// DNS01 is the "dns-01" ACME challenge https://www.rfc-editor.org/rfc/rfc8555.html#section-8.4 // DNS01 is the "dns-01" ACME challenge https://tools.ietf.org/html/rfc8555#section-8.4
// Note: GetRecord returns a DNS record which will fulfill this challenge. // Note: GetRecord returns a DNS record which will fulfill this challenge.
DNS01 = Type("dns-01") DNS01 = Type("dns-01")
// TLSALPN01 is the "tls-alpn-01" ACME challenge https://www.rfc-editor.org/rfc/rfc8737.html // TLSALPN01 is the "tls-alpn-01" ACME challenge https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-07
TLSALPN01 = Type("tls-alpn-01") TLSALPN01 = Type("tls-alpn-01")
) )

View File

@ -1,16 +1,12 @@
package dns01 package dns01
import ( import "github.com/miekg/dns"
"strings"
"github.com/miekg/dns"
)
// Update FQDN with CNAME if any. // Update FQDN with CNAME if any.
func updateDomainWithCName(r *dns.Msg, fqdn string) string { func updateDomainWithCName(r *dns.Msg, fqdn string) string {
for _, rr := range r.Answer { for _, rr := range r.Answer {
if cn, ok := rr.(*dns.CNAME); ok { if cn, ok := rr.(*dns.CNAME); ok {
if strings.EqualFold(cn.Hdr.Name, fqdn) { if cn.Hdr.Name == fqdn {
return cn.Target return cn.Target
} }
} }

View File

@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"os" "os"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme"
@ -115,7 +114,7 @@ func (c *Challenge) Solve(authz acme.Authorization) error {
return err return err
} }
info := GetChallengeInfo(authz.Identifier.Value, keyAuth) fqdn, value := GetRecord(authz.Identifier.Value, keyAuth)
var timeout, interval time.Duration var timeout, interval time.Duration
switch provider := c.provider.(type) { switch provider := c.provider.(type) {
@ -125,12 +124,12 @@ func (c *Challenge) Solve(authz acme.Authorization) error {
timeout, interval = DefaultPropagationTimeout, DefaultPollingInterval timeout, interval = DefaultPropagationTimeout, DefaultPollingInterval
} }
log.Infof("[%s] acme: Checking DNS record propagation. [nameservers=%s]", domain, strings.Join(recursiveNameservers, ",")) log.Infof("[%s] acme: Checking DNS record propagation using %+v", domain, recursiveNameservers)
time.Sleep(interval) time.Sleep(interval)
err = wait.For("propagation", timeout, interval, func() (bool, error) { err = wait.For("propagation", timeout, interval, func() (bool, error) {
stop, errP := c.preCheck.call(domain, info.EffectiveFQDN, info.Value) stop, errP := c.preCheck.call(domain, fqdn, value)
if !stop || errP != nil { if !stop || errP != nil {
log.Infof("[%s] acme: Waiting for DNS record propagation.", domain) log.Infof("[%s] acme: Waiting for DNS record propagation.", domain)
} }
@ -173,67 +172,19 @@ type sequential interface {
} }
// GetRecord returns a DNS record which will fulfill the `dns-01` challenge. // GetRecord returns a DNS record which will fulfill the `dns-01` challenge.
// Deprecated: use GetChallengeInfo instead.
func GetRecord(domain, keyAuth string) (fqdn, value string) { func GetRecord(domain, keyAuth string) (fqdn, value string) {
info := GetChallengeInfo(domain, keyAuth)
return info.EffectiveFQDN, info.Value
}
// ChallengeInfo contains the information use to create the TXT record.
type ChallengeInfo struct {
// FQDN is the full-qualified challenge domain (i.e. `_acme-challenge.[domain].`)
FQDN string
// EffectiveFQDN contains the resulting FQDN after the CNAMEs resolutions.
EffectiveFQDN string
// Value contains the value for the TXT record.
Value string
}
// GetChallengeInfo returns information used to create a DNS record which will fulfill the `dns-01` challenge.
func GetChallengeInfo(domain, keyAuth string) ChallengeInfo {
keyAuthShaBytes := sha256.Sum256([]byte(keyAuth)) keyAuthShaBytes := sha256.Sum256([]byte(keyAuth))
// base64URL encoding without padding // base64URL encoding without padding
value := base64.RawURLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size]) value = base64.RawURLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size])
fqdn = fmt.Sprintf("_acme-challenge.%s.", domain)
ok, _ := strconv.ParseBool(os.Getenv("LEGO_DISABLE_CNAME_SUPPORT")) if ok, _ := strconv.ParseBool(os.Getenv("LEGO_EXPERIMENTAL_CNAME_SUPPORT")); ok {
return ChallengeInfo{
Value: value,
FQDN: getChallengeFQDN(domain, false),
EffectiveFQDN: getChallengeFQDN(domain, !ok),
}
}
func getChallengeFQDN(domain string, followCNAME bool) string {
fqdn := fmt.Sprintf("_acme-challenge.%s.", domain)
if !followCNAME {
return fqdn
}
// recursion counter so it doesn't spin out of control
for range 50 {
// Keep following CNAMEs
r, err := dnsQuery(fqdn, dns.TypeCNAME, recursiveNameservers, true) r, err := dnsQuery(fqdn, dns.TypeCNAME, recursiveNameservers, true)
// Check if the domain has CNAME then return that
if err != nil || r.Rcode != dns.RcodeSuccess { if err == nil && r.Rcode == dns.RcodeSuccess {
// No more CNAME records to follow, exit fqdn = updateDomainWithCName(r, fqdn)
break
} }
// Check if the domain has CNAME then use that
cname := updateDomainWithCName(r, fqdn)
if cname == fqdn {
break
}
log.Infof("Found CNAME entry for %q: %q", fqdn, cname)
fqdn = cname
} }
return fqdn return
} }

View File

@ -8,7 +8,7 @@ import (
) )
const ( const (
dnsTemplate = `%s %d IN TXT %q` dnsTemplate = `%s %d IN TXT "%s"`
) )
// DNSProviderManual is an implementation of the ChallengeProvider interface. // DNSProviderManual is an implementation of the ChallengeProvider interface.
@ -21,36 +21,33 @@ func NewDNSProviderManual() (*DNSProviderManual, error) {
// Present prints instructions for manually creating the TXT record. // Present prints instructions for manually creating the TXT record.
func (*DNSProviderManual) Present(domain, token, keyAuth string) error { func (*DNSProviderManual) Present(domain, token, keyAuth string) error {
info := GetChallengeInfo(domain, keyAuth) fqdn, value := GetRecord(domain, keyAuth)
authZone, err := FindZoneByFqdn(info.EffectiveFQDN) authZone, err := FindZoneByFqdn(fqdn)
if err != nil { if err != nil {
return fmt.Errorf("manual: could not find zone: %w", err) return err
} }
fmt.Printf("lego: Please create the following TXT record in your %s zone:\n", authZone) fmt.Printf("lego: Please create the following TXT record in your %s zone:\n", authZone)
fmt.Printf(dnsTemplate+"\n", info.EffectiveFQDN, DefaultTTL, info.Value) fmt.Printf(dnsTemplate+"\n", fqdn, DefaultTTL, value)
fmt.Printf("lego: Press 'Enter' when you are done\n") fmt.Printf("lego: Press 'Enter' when you are done\n")
_, err = bufio.NewReader(os.Stdin).ReadBytes('\n') _, err = bufio.NewReader(os.Stdin).ReadBytes('\n')
if err != nil {
return fmt.Errorf("manual: %w", err)
}
return nil return err
} }
// CleanUp prints instructions for manually removing the TXT record. // CleanUp prints instructions for manually removing the TXT record.
func (*DNSProviderManual) CleanUp(domain, token, keyAuth string) error { func (*DNSProviderManual) CleanUp(domain, token, keyAuth string) error {
info := GetChallengeInfo(domain, keyAuth) fqdn, _ := GetRecord(domain, keyAuth)
authZone, err := FindZoneByFqdn(info.EffectiveFQDN) authZone, err := FindZoneByFqdn(fqdn)
if err != nil { if err != nil {
return fmt.Errorf("manual: could not find zone: %w", err) return err
} }
fmt.Printf("lego: You can now remove this TXT record from your %s zone:\n", authZone) fmt.Printf("lego: You can now remove this TXT record from your %s zone:\n", authZone)
fmt.Printf(dnsTemplate+"\n", info.EffectiveFQDN, DefaultTTL, "...") fmt.Printf(dnsTemplate+"\n", fqdn, DefaultTTL, "...")
return nil return nil
} }

View File

@ -1,24 +0,0 @@
package dns01
import (
"fmt"
"strings"
"github.com/miekg/dns"
)
// ExtractSubDomain extracts the subdomain part from a domain and a zone.
func ExtractSubDomain(domain, zone string) (string, error) {
canonDomain := dns.Fqdn(domain)
canonZone := dns.Fqdn(zone)
if canonDomain == canonZone {
return "", fmt.Errorf("no subdomain because the domain and the zone are identical: %s", canonDomain)
}
if !dns.IsSubDomain(canonZone, canonDomain) {
return "", fmt.Errorf("%s is not a subdomain of %s", canonDomain, canonZone)
}
return strings.TrimSuffix(canonDomain, "."+canonZone), nil
}

View File

@ -4,9 +4,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"net" "net"
"os"
"slices"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -16,6 +13,9 @@ import (
const defaultResolvConf = "/etc/resolv.conf" const defaultResolvConf = "/etc/resolv.conf"
// dnsTimeout is used to override the default DNS timeout of 10 seconds.
var dnsTimeout = 10 * time.Second
var ( var (
fqdnSoaCache = map[string]*soaCacheEntry{} fqdnSoaCache = map[string]*soaCacheEntry{}
muFqdnSoaCache sync.Mutex muFqdnSoaCache sync.Mutex
@ -99,12 +99,12 @@ func lookupNameservers(fqdn string) ([]string, error) {
zone, err := FindZoneByFqdn(fqdn) zone, err := FindZoneByFqdn(fqdn)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not find zone: %w", err) return nil, fmt.Errorf("could not determine the zone: %w", err)
} }
r, err := dnsQuery(zone, dns.TypeNS, recursiveNameservers, true) r, err := dnsQuery(zone, dns.TypeNS, recursiveNameservers, true)
if err != nil { if err != nil {
return nil, fmt.Errorf("NS call failed: %w", err) return nil, err
} }
for _, rr := range r.Answer { for _, rr := range r.Answer {
@ -116,8 +116,7 @@ func lookupNameservers(fqdn string) ([]string, error) {
if len(authoritativeNss) > 0 { if len(authoritativeNss) > 0 {
return authoritativeNss, nil return authoritativeNss, nil
} }
return nil, errors.New("could not determine authoritative nameservers")
return nil, fmt.Errorf("[zone=%s] could not determine authoritative nameservers", zone)
} }
// FindPrimaryNsByFqdn determines the primary nameserver of the zone apex for the given fqdn // FindPrimaryNsByFqdn determines the primary nameserver of the zone apex for the given fqdn
@ -131,7 +130,7 @@ func FindPrimaryNsByFqdn(fqdn string) (string, error) {
func FindPrimaryNsByFqdnCustom(fqdn string, nameservers []string) (string, error) { func FindPrimaryNsByFqdnCustom(fqdn string, nameservers []string) (string, error) {
soa, err := lookupSoaByFqdn(fqdn, nameservers) soa, err := lookupSoaByFqdn(fqdn, nameservers)
if err != nil { if err != nil {
return "", fmt.Errorf("[fqdn=%s] %w", fqdn, err) return "", err
} }
return soa.primaryNs, nil return soa.primaryNs, nil
} }
@ -147,7 +146,7 @@ func FindZoneByFqdn(fqdn string) (string, error) {
func FindZoneByFqdnCustom(fqdn string, nameservers []string) (string, error) { func FindZoneByFqdnCustom(fqdn string, nameservers []string) (string, error) {
soa, err := lookupSoaByFqdn(fqdn, nameservers) soa, err := lookupSoaByFqdn(fqdn, nameservers)
if err != nil { if err != nil {
return "", fmt.Errorf("[fqdn=%s] %w", fqdn, err) return "", err
} }
return soa.zone, nil return soa.zone, nil
} }
@ -172,35 +171,35 @@ func lookupSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error)
func fetchSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error) { func fetchSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error) {
var err error var err error
var r *dns.Msg var in *dns.Msg
labelIndexes := dns.Split(fqdn) labelIndexes := dns.Split(fqdn)
for _, index := range labelIndexes { for _, index := range labelIndexes {
domain := fqdn[index:] domain := fqdn[index:]
r, err = dnsQuery(domain, dns.TypeSOA, nameservers, true) in, err = dnsQuery(domain, dns.TypeSOA, nameservers, true)
if err != nil { if err != nil {
continue continue
} }
if r == nil { if in == nil {
continue continue
} }
switch r.Rcode { switch in.Rcode {
case dns.RcodeSuccess: case dns.RcodeSuccess:
// Check if we got a SOA RR in the answer section // Check if we got a SOA RR in the answer section
if len(r.Answer) == 0 { if len(in.Answer) == 0 {
continue continue
} }
// CNAME records cannot/should not exist at the root of a zone. // CNAME records cannot/should not exist at the root of a zone.
// So we skip a domain when a CNAME is found. // So we skip a domain when a CNAME is found.
if dnsMsgContainsCNAME(r) { if dnsMsgContainsCNAME(in) {
continue continue
} }
for _, ans := range r.Answer { for _, ans := range in.Answer {
if soa, ok := ans.(*dns.SOA); ok { if soa, ok := ans.(*dns.SOA); ok {
return newSoaCacheEntry(soa), nil return newSoaCacheEntry(soa), nil
} }
@ -209,46 +208,36 @@ func fetchSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error) {
// NXDOMAIN // NXDOMAIN
default: default:
// Any response code other than NOERROR and NXDOMAIN is treated as error // Any response code other than NOERROR and NXDOMAIN is treated as error
return nil, &DNSError{Message: fmt.Sprintf("unexpected response for '%s'", domain), MsgOut: r} return nil, fmt.Errorf("unexpected response code '%s' for %s", dns.RcodeToString[in.Rcode], domain)
} }
} }
return nil, &DNSError{Message: fmt.Sprintf("could not find the start of authority for '%s'", fqdn), MsgOut: r, Err: err} return nil, fmt.Errorf("could not find the start of authority for %s%s", fqdn, formatDNSError(in, err))
} }
// dnsMsgContainsCNAME checks for a CNAME answer in msg. // dnsMsgContainsCNAME checks for a CNAME answer in msg.
func dnsMsgContainsCNAME(msg *dns.Msg) bool { func dnsMsgContainsCNAME(msg *dns.Msg) bool {
return slices.ContainsFunc(msg.Answer, func(rr dns.RR) bool { for _, ans := range msg.Answer {
_, ok := rr.(*dns.CNAME) if _, ok := ans.(*dns.CNAME); ok {
return ok return true
}) }
}
return false
} }
func dnsQuery(fqdn string, rtype uint16, nameservers []string, recursive bool) (*dns.Msg, error) { func dnsQuery(fqdn string, rtype uint16, nameservers []string, recursive bool) (*dns.Msg, error) {
m := createDNSMsg(fqdn, rtype, recursive) m := createDNSMsg(fqdn, rtype, recursive)
if len(nameservers) == 0 { var in *dns.Msg
return nil, &DNSError{Message: "empty list of nameservers"}
}
var r *dns.Msg
var err error var err error
var errAll error
for _, ns := range nameservers { for _, ns := range nameservers {
r, err = sendDNSQuery(m, ns) in, err = sendDNSQuery(m, ns)
if err == nil && len(r.Answer) > 0 { if err == nil && len(in.Answer) > 0 {
break break
} }
errAll = errors.Join(errAll, err)
} }
return in, err
if err != nil {
return r, errAll
}
return r, nil
} }
func createDNSMsg(fqdn string, rtype uint16, recursive bool) *dns.Msg { func createDNSMsg(fqdn string, rtype uint16, recursive bool) *dns.Msg {
@ -264,84 +253,32 @@ func createDNSMsg(fqdn string, rtype uint16, recursive bool) *dns.Msg {
} }
func sendDNSQuery(m *dns.Msg, ns string) (*dns.Msg, error) { func sendDNSQuery(m *dns.Msg, ns string) (*dns.Msg, error) {
if ok, _ := strconv.ParseBool(os.Getenv("LEGO_EXPERIMENTAL_DNS_TCP_ONLY")); ok { udp := &dns.Client{Net: "udp", Timeout: dnsTimeout}
tcp := &dns.Client{Net: "tcp", Timeout: dnsTimeout} in, _, err := udp.Exchange(m, ns)
r, _, err := tcp.Exchange(m, ns)
if err != nil {
return r, &DNSError{Message: "DNS call error", MsgIn: m, NS: ns, Err: err}
}
return r, nil if in != nil && in.Truncated {
tcp := &dns.Client{Net: "tcp", Timeout: dnsTimeout}
// If the TCP request succeeds, the err will reset to nil
in, _, err = tcp.Exchange(m, ns)
} }
udp := &dns.Client{Net: "udp", Timeout: dnsTimeout} return in, err
r, _, err := udp.Exchange(m, ns) }
if r != nil && r.Truncated { func formatDNSError(msg *dns.Msg, err error) string {
tcp := &dns.Client{Net: "tcp", Timeout: dnsTimeout} var parts []string
// If the TCP request succeeds, the "err" will reset to nil
r, _, err = tcp.Exchange(m, ns) if msg != nil {
parts = append(parts, dns.RcodeToString[msg.Rcode])
} }
if err != nil { if err != nil {
return r, &DNSError{Message: "DNS call error", MsgIn: m, NS: ns, Err: err} parts = append(parts, err.Error())
} }
return r, nil if len(parts) > 0 {
} return ": " + strings.Join(parts, " ")
}
// DNSError error related to DNS calls.
type DNSError struct { return ""
Message string
NS string
MsgIn *dns.Msg
MsgOut *dns.Msg
Err error
}
func (d *DNSError) Error() string {
var details []string
if d.NS != "" {
details = append(details, "ns="+d.NS)
}
if d.MsgIn != nil && len(d.MsgIn.Question) > 0 {
details = append(details, fmt.Sprintf("question='%s'", formatQuestions(d.MsgIn.Question)))
}
if d.MsgOut != nil {
if d.MsgIn == nil || len(d.MsgIn.Question) == 0 {
details = append(details, fmt.Sprintf("question='%s'", formatQuestions(d.MsgOut.Question)))
}
details = append(details, "code="+dns.RcodeToString[d.MsgOut.Rcode])
}
msg := "DNS error"
if d.Message != "" {
msg = d.Message
}
if d.Err != nil {
msg += ": " + d.Err.Error()
}
if len(details) > 0 {
msg += " [" + strings.Join(details, ", ") + "]"
}
return msg
}
func (d *DNSError) Unwrap() error {
return d.Err
}
func formatQuestions(questions []dns.Question) string {
var parts []string
for _, question := range questions {
parts = append(parts, strings.ReplaceAll(strings.TrimPrefix(question.String(), ";"), "\t", " "))
}
return strings.Join(parts, ";")
} }

View File

@ -1,8 +0,0 @@
//go:build !windows
package dns01
import "time"
// dnsTimeout is used to override the default DNS timeout of 10 seconds.
var dnsTimeout = 10 * time.Second

View File

@ -1,8 +0,0 @@
//go:build windows
package dns01
import "time"
// dnsTimeout is used to override the default DNS timeout of 20 seconds.
var dnsTimeout = 20 * time.Second

View File

@ -23,17 +23,14 @@ import (
// RFC7239 has standardized the different forwarding headers into a single header named Forwarded. // RFC7239 has standardized the different forwarding headers into a single header named Forwarded.
// The header value has a different format, so you should use forwardedMatcher // The header value has a different format, so you should use forwardedMatcher
// when the http01.ProviderServer operates behind a RFC7239 compatible proxy. // when the http01.ProviderServer operates behind a RFC7239 compatible proxy.
// https://www.rfc-editor.org/rfc/rfc7239.html // https://tools.ietf.org/html/rfc7239
// //
// Note: RFC7239 also reminds us, "that an HTTP list [...] may be split over multiple header fields" (section 7.1), // Note: RFC7239 also reminds us, "that an HTTP list [...] may be split over multiple header fields" (section 7.1),
// meaning that // meaning that
// // X-Header: a
// X-Header: a // X-Header: b
// X-Header: b
//
// is equal to // is equal to
// // X-Header: a, b
// X-Header: a, b
// //
// All matcher implementations (explicitly not excluding arbitraryMatcher!) // All matcher implementations (explicitly not excluding arbitraryMatcher!)
// have in common that they only match against the first value in such lists. // have in common that they only match against the first value in such lists.
@ -69,7 +66,7 @@ func (m arbitraryMatcher) matches(r *http.Request, domain string) bool {
} }
// forwardedMatcher checks whether the Forwarded header contains a "host" element starting with a domain name. // forwardedMatcher checks whether the Forwarded header contains a "host" element starting with a domain name.
// See https://www.rfc-editor.org/rfc/rfc7239.html for details. // See https://tools.ietf.org/html/rfc7239 for details.
type forwardedMatcher struct{} type forwardedMatcher struct{}
func (m *forwardedMatcher) name() string { func (m *forwardedMatcher) name() string {

View File

@ -2,11 +2,9 @@ package http01
import ( import (
"fmt" "fmt"
"io/fs"
"net" "net"
"net/http" "net/http"
"net/textproto" "net/textproto"
"os"
"strings" "strings"
"github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/log"
@ -16,11 +14,8 @@ import (
// It may be instantiated without using the NewProviderServer function if // It may be instantiated without using the NewProviderServer function if
// you want only to use the default values. // you want only to use the default values.
type ProviderServer struct { type ProviderServer struct {
address string iface string
network string // must be valid argument to net.Listen port string
socketMode fs.FileMode
matcher domainMatcher matcher domainMatcher
done chan bool done chan bool
listener net.Listener listener net.Listener
@ -34,34 +29,24 @@ func NewProviderServer(iface, port string) *ProviderServer {
port = "80" port = "80"
} }
return &ProviderServer{network: "tcp", address: net.JoinHostPort(iface, port), matcher: &hostMatcher{}} return &ProviderServer{iface: iface, port: port, matcher: &hostMatcher{}}
}
func NewUnixProviderServer(socketPath string, mode fs.FileMode) *ProviderServer {
return &ProviderServer{network: "unix", address: socketPath, socketMode: mode, matcher: &hostMatcher{}}
} }
// Present starts a web server and makes the token available at `ChallengePath(token)` for web requests. // Present starts a web server and makes the token available at `ChallengePath(token)` for web requests.
func (s *ProviderServer) Present(domain, token, keyAuth string) error { func (s *ProviderServer) Present(domain, token, keyAuth string) error {
var err error var err error
s.listener, err = net.Listen(s.network, s.GetAddress()) s.listener, err = net.Listen("tcp", s.GetAddress())
if err != nil { if err != nil {
return fmt.Errorf("could not start HTTP server for challenge: %w", err) return fmt.Errorf("could not start HTTP server for challenge: %w", err)
} }
if s.network == "unix" {
if err = os.Chmod(s.address, s.socketMode); err != nil {
return fmt.Errorf("chmod %s: %w", s.address, err)
}
}
s.done = make(chan bool) s.done = make(chan bool)
go s.serve(domain, token, keyAuth) go s.serve(domain, token, keyAuth)
return nil return nil
} }
func (s *ProviderServer) GetAddress() string { func (s *ProviderServer) GetAddress() string {
return s.address return net.JoinHostPort(s.iface, s.port)
} }
// CleanUp closes the HTTP server and removes the token from `ChallengePath(token)`. // CleanUp closes the HTTP server and removes the token from `ChallengePath(token)`.
@ -84,7 +69,7 @@ func (s *ProviderServer) CleanUp(domain, token, keyAuth string) error {
// //
// The exact behavior depends on the value of headerName: // The exact behavior depends on the value of headerName:
// - "" (the empty string) and "Host" will restore the default and only check the Host header // - "" (the empty string) and "Host" will restore the default and only check the Host header
// - "Forwarded" will look for a Forwarded header, and inspect it according to https://www.rfc-editor.org/rfc/rfc7239.html // - "Forwarded" will look for a Forwarded header, and inspect it according to https://tools.ietf.org/html/rfc7239
// - any other value will check the header value with the same name. // - any other value will check the header value with the same name.
func (s *ProviderServer) SetProxyHeader(headerName string) { func (s *ProviderServer) SetProxyHeader(headerName string) {
switch h := textproto.CanonicalMIMEHeaderKey(headerName); h { switch h := textproto.CanonicalMIMEHeaderKey(headerName); h {
@ -100,7 +85,7 @@ func (s *ProviderServer) SetProxyHeader(headerName string) {
func (s *ProviderServer) serve(domain, token, keyAuth string) { func (s *ProviderServer) serve(domain, token, keyAuth string) {
path := ChallengePath(token) path := ChallengePath(token)
// The incoming request will be validated to prevent DNS rebind attacks. // The incoming request must will be validated to prevent DNS rebind attacks.
// We only respond with the keyAuth, when we're receiving a GET requests with // We only respond with the keyAuth, when we're receiving a GET requests with
// the "Host" header matching the domain (the latter is configurable though SetProxyHeader). // the "Host" header matching the domain (the latter is configurable though SetProxyHeader).
mux := http.NewServeMux() mux := http.NewServeMux()
@ -114,7 +99,7 @@ func (s *ProviderServer) serve(domain, token, keyAuth string) {
} }
log.Infof("[%s] Served key authentication", domain) log.Infof("[%s] Served key authentication", domain)
} else { } else {
log.Warnf("Received request for domain %s with method %s but the domain did not match any challenge. Please ensure you are passing the %s header properly.", r.Host, r.Method, s.matcher.name()) log.Warnf("Received request for domain %s with method %s but the domain did not match any challenge. Please ensure your are passing the %s header properly.", r.Host, r.Method, s.matcher.name())
_, err := w.Write([]byte("TEST")) _, err := w.Write([]byte("TEST"))
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)

View File

@ -19,7 +19,7 @@ func (e obtainError) Error() string {
sort.Strings(domains) sort.Strings(domains)
for _, domain := range domains { for _, domain := range domains {
_, _ = fmt.Fprintf(buffer, "[%s] %s\n", domain, e[domain]) buffer.WriteString(fmt.Sprintf("[%s] %s\n", domain, e[domain]))
} }
return buffer.String() return buffer.String()
} }

View File

@ -128,7 +128,7 @@ func sequentialSolve(authSolvers []*selectedAuthSolver, failures obtainError) {
} }
func parallelSolve(authSolvers []*selectedAuthSolver, failures obtainError) { func parallelSolve(authSolvers []*selectedAuthSolver, failures obtainError) {
// For all valid preSolvers, first submit the challenges, so they have max time to propagate // For all valid preSolvers, first submit the challenges so they have max time to propagate
for _, authSolver := range authSolvers { for _, authSolver := range authSolvers {
authz := authSolver.authz authz := authSolver.authz
if solvr, ok := authSolver.solver.(preSolver); ok { if solvr, ok := authSolver.solver.(preSolver); ok {

View File

@ -1,6 +1,7 @@
package resolver package resolver
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"sort" "sort"
@ -53,7 +54,7 @@ func (c *SolverManager) SetDNS01Provider(p challenge.Provider, opts ...dns01.Cha
return nil return nil
} }
// Remove removes a challenge type from the available solvers. // Remove Remove a challenge type from the available solvers.
func (c *SolverManager) Remove(chlgType challenge.Type) { func (c *SolverManager) Remove(chlgType challenge.Type) {
delete(c.solvers, chlgType) delete(c.solvers, chlgType)
} }
@ -106,17 +107,21 @@ func validate(core *api.Core, domain string, chlg acme.Challenge) error {
bo.MaxInterval = 10 * initialInterval bo.MaxInterval = 10 * initialInterval
bo.MaxElapsedTime = 100 * initialInterval bo.MaxElapsedTime = 100 * initialInterval
ctx, cancel := context.WithCancel(context.Background())
// After the path is sent, the ACME server will access our server. // After the path is sent, the ACME server will access our server.
// Repeatedly check the server for an updated status on our request. // Repeatedly check the server for an updated status on our request.
operation := func() error { operation := func() error {
authz, err := core.Authorizations.Get(chlng.AuthorizationURL) authz, err := core.Authorizations.Get(chlng.AuthorizationURL)
if err != nil { if err != nil {
return backoff.Permanent(err) cancel()
return err
} }
valid, err := checkAuthorizationStatus(authz) valid, err := checkAuthorizationStatus(authz)
if err != nil { if err != nil {
return backoff.Permanent(err) cancel()
return err
} }
if valid { if valid {
@ -127,7 +132,7 @@ func validate(core *api.Core, domain string, chlg acme.Challenge) error {
return errors.New("the server didn't respond to our request") return errors.New("the server didn't respond to our request")
} }
return backoff.Retry(operation, bo) return backoff.Retry(operation, backoff.WithContext(bo, ctx))
} }
func checkChallengeStatus(chlng acme.ExtendedChallenge) (bool, error) { func checkChallengeStatus(chlng acme.ExtendedChallenge) (bool, error) {

View File

@ -16,7 +16,7 @@ import (
) )
// idPeAcmeIdentifierV1 is the SMI Security for PKIX Certification Extension OID referencing the ACME extension. // idPeAcmeIdentifierV1 is the SMI Security for PKIX Certification Extension OID referencing the ACME extension.
// Reference: https://www.rfc-editor.org/rfc/rfc8737.html#section-6.1 // Reference: https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-07#section-6.1
var idPeAcmeIdentifierV1 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31} var idPeAcmeIdentifierV1 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31}
type ValidateFunc func(core *api.Core, domain string, chlng acme.Challenge) error type ValidateFunc func(core *api.Core, domain string, chlng acme.Challenge) error
@ -83,7 +83,7 @@ func ChallengeBlocks(domain, keyAuth string) ([]byte, []byte, error) {
// Add the keyAuth digest as the acmeValidation-v1 extension // Add the keyAuth digest as the acmeValidation-v1 extension
// (marked as critical such that it won't be used by non-ACME software). // (marked as critical such that it won't be used by non-ACME software).
// Reference: https://www.rfc-editor.org/rfc/rfc8737.html#section-3 // Reference: https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-07#section-3
extensions := []pkix.Extension{ extensions := []pkix.Extension{
{ {
Id: idPeAcmeIdentifierV1, Id: idPeAcmeIdentifierV1,

View File

@ -40,7 +40,7 @@ func (s *ProviderServer) GetAddress() string {
return net.JoinHostPort(s.iface, s.port) return net.JoinHostPort(s.iface, s.port)
} }
// Present generates a certificate with an SHA-256 digest of the keyAuth provided // Present generates a certificate with a SHA-256 digest of the keyAuth provided
// as the acmeValidation-v1 extension value to conform to the ACME-TLS-ALPN spec. // as the acmeValidation-v1 extension value to conform to the ACME-TLS-ALPN spec.
func (s *ProviderServer) Present(domain, token, keyAuth string) error { func (s *ProviderServer) Present(domain, token, keyAuth string) error {
if s.port == "" { if s.port == "" {
@ -61,7 +61,7 @@ func (s *ProviderServer) Present(domain, token, keyAuth string) error {
// We must set that the `acme-tls/1` application level protocol is supported // We must set that the `acme-tls/1` application level protocol is supported
// so that the protocol negotiation can succeed. Reference: // so that the protocol negotiation can succeed. Reference:
// https://www.rfc-editor.org/rfc/rfc8737.html#section-6.2 // https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-07#section-6.2
tlsConf.NextProtos = []string{ACMETLS1Protocol} tlsConf.NextProtos = []string{ACMETLS1Protocol}
// Create the listener with the created tls.Config. // Create the listener with the created tls.Config.

View File

@ -4,11 +4,10 @@ import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"fmt" "fmt"
"io/ioutil"
"net" "net"
"net/http" "net/http"
"os" "os"
"strconv"
"strings"
"time" "time"
"github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/certcrypto"
@ -18,18 +17,13 @@ import (
const ( const (
// caCertificatesEnvVar is the environment variable name that can be used to // caCertificatesEnvVar is the environment variable name that can be used to
// specify the path to PEM encoded CA Certificates that can be used to // specify the path to PEM encoded CA Certificates that can be used to
// authenticate an ACME server with an HTTPS certificate not issued by a CA in // authenticate an ACME server with a HTTPS certificate not issued by a CA in
// the system-wide trusted root list. // the system-wide trusted root list.
// Multiple file paths can be added by using os.PathListSeparator as a separator.
caCertificatesEnvVar = "LEGO_CA_CERTIFICATES" caCertificatesEnvVar = "LEGO_CA_CERTIFICATES"
// caSystemCertPool is the environment variable name that can be used to define
// if the certificates pool must use a copy of the system cert pool.
caSystemCertPool = "LEGO_CA_SYSTEM_CERT_POOL"
// caServerNameEnvVar is the environment variable name that can be used to // caServerNameEnvVar is the environment variable name that can be used to
// specify the CA server name that can be used to // specify the CA server name that can be used to
// authenticate an ACME server with an HTTPS certificate not issued by a CA in // authenticate an ACME server with a HTTPS certificate not issued by a CA in
// the system-wide trusted root list. // the system-wide trusted root list.
caServerNameEnvVar = "LEGO_CA_SERVER_NAME" caServerNameEnvVar = "LEGO_CA_SERVER_NAME"
@ -70,15 +64,15 @@ type CertificateConfig struct {
// based on the caCertificatesEnvVar environment variable (see the `initCertPool` function). // based on the caCertificatesEnvVar environment variable (see the `initCertPool` function).
func createDefaultHTTPClient() *http.Client { func createDefaultHTTPClient() *http.Client {
return &http.Client{ return &http.Client{
Timeout: 2 * time.Minute,
Transport: &http.Transport{ Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment, Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{ DialContext: (&net.Dialer{
Timeout: 30 * time.Second, Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second, KeepAlive: 30 * time.Second,
}).DialContext, }).DialContext,
TLSHandshakeTimeout: 30 * time.Second, TLSHandshakeTimeout: 15 * time.Second,
ResponseHeaderTimeout: 30 * time.Second, ResponseHeaderTimeout: 15 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{ TLSClientConfig: &tls.Config{
ServerName: os.Getenv(caServerNameEnvVar), ServerName: os.Getenv(caServerNameEnvVar),
RootCAs: initCertPool(), RootCAs: initCertPool(),
@ -88,44 +82,23 @@ func createDefaultHTTPClient() *http.Client {
} }
// initCertPool creates a *x509.CertPool populated with the PEM certificates // initCertPool creates a *x509.CertPool populated with the PEM certificates
// found in the filepath specified in the caCertificatesEnvVar OS environment variable. // found in the filepath specified in the caCertificatesEnvVar OS environment
// If the caCertificatesEnvVar is not set then initCertPool will return nil. // variable. If the caCertificatesEnvVar is not set then initCertPool will
// If there is an error creating a *x509.CertPool from the provided caCertificatesEnvVar value then initCertPool will panic. // return nil. If there is an error creating a *x509.CertPool from the provided
// If the caSystemCertPool is set to a "truthy value" (`1`, `t`, `T`, `TRUE`, `true`, `True`) then a copy of system cert pool will be used. // caCertificatesEnvVar value then initCertPool will panic.
// caSystemCertPool requires caCertificatesEnvVar to be set.
func initCertPool() *x509.CertPool { func initCertPool() *x509.CertPool {
customCACertsPath := os.Getenv(caCertificatesEnvVar) if customCACertsPath := os.Getenv(caCertificatesEnvVar); customCACertsPath != "" {
if customCACertsPath == "" { customCAs, err := ioutil.ReadFile(customCACertsPath)
return nil
}
certPool := getCertPool()
for _, customPath := range strings.Split(customCACertsPath, string(os.PathListSeparator)) {
customCAs, err := os.ReadFile(customPath)
if err != nil { if err != nil {
panic(fmt.Sprintf("error reading %s=%q: %v", panic(fmt.Sprintf("error reading %s=%q: %v",
caCertificatesEnvVar, customPath, err)) caCertificatesEnvVar, customCACertsPath, err))
} }
certPool := x509.NewCertPool()
if ok := certPool.AppendCertsFromPEM(customCAs); !ok { if ok := certPool.AppendCertsFromPEM(customCAs); !ok {
panic(fmt.Sprintf("error creating x509 cert pool from %s=%q: %v", panic(fmt.Sprintf("error creating x509 cert pool from %s=%q: %v",
caCertificatesEnvVar, customPath, err)) caCertificatesEnvVar, customCACertsPath, err))
} }
return certPool
} }
return nil
return certPool
}
func getCertPool() *x509.CertPool {
useSystemCertPool, _ := strconv.ParseBool(os.Getenv(caSystemCertPool))
if !useSystemCertPool {
return x509.NewCertPool()
}
pool, err := x509.SystemCertPool()
if err == nil {
return pool
}
return x509.NewCertPool()
} }

View File

@ -6,7 +6,7 @@ import (
) )
// Logger is an optional custom logger. // Logger is an optional custom logger.
var Logger StdLogger = log.New(os.Stderr, "", log.LstdFlags) var Logger StdLogger = log.New(os.Stdout, "", log.LstdFlags)
// StdLogger interface for Standard Logger. // StdLogger interface for Standard Logger.
type StdLogger interface { type StdLogger interface {

View File

@ -3,6 +3,7 @@ package env
import ( import (
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"strconv" "strconv"
"strings" "strings"
@ -31,29 +32,28 @@ func Get(names ...string) (map[string]string, error) {
return values, nil return values, nil
} }
// GetWithFallback Get environment variable values. // GetWithFallback Get environment variable values
// The first name in each group is use as key in the result map. // The first name in each group is use as key in the result map
//
// case 1:
// //
// // LEGO_ONE="ONE" // // LEGO_ONE="ONE"
// // LEGO_TWO="TWO" // // LEGO_TWO="TWO"
// env.GetWithFallback([]string{"LEGO_ONE", "LEGO_TWO"}) // env.GetWithFallback([]string{"LEGO_ONE", "LEGO_TWO"})
// // => "LEGO_ONE" = "ONE" // // => "LEGO_ONE" = "ONE"
// //
// case 2: // ----
// //
// // LEGO_ONE="" // // LEGO_ONE=""
// // LEGO_TWO="TWO" // // LEGO_TWO="TWO"
// env.GetWithFallback([]string{"LEGO_ONE", "LEGO_TWO"}) // env.GetWithFallback([]string{"LEGO_ONE", "LEGO_TWO"})
// // => "LEGO_ONE" = "TWO" // // => "LEGO_ONE" = "TWO"
// //
// case 3: // ----
// //
// // LEGO_ONE="" // // LEGO_ONE=""
// // LEGO_TWO="" // // LEGO_TWO=""
// env.GetWithFallback([]string{"LEGO_ONE", "LEGO_TWO"}) // env.GetWithFallback([]string{"LEGO_ONE", "LEGO_TWO"})
// // => error // // => error
//
func GetWithFallback(groups ...[]string) (map[string]string, error) { func GetWithFallback(groups ...[]string) (map[string]string, error) {
values := map[string]string{} values := map[string]string{}
@ -64,7 +64,7 @@ func GetWithFallback(groups ...[]string) (map[string]string, error) {
} }
value, envVar := getOneWithFallback(names[0], names[1:]...) value, envVar := getOneWithFallback(names[0], names[1:]...)
if value == "" { if len(value) == 0 {
missingEnvVars = append(missingEnvVars, envVar) missingEnvVars = append(missingEnvVars, envVar)
continue continue
} }
@ -78,26 +78,15 @@ func GetWithFallback(groups ...[]string) (map[string]string, error) {
return values, nil return values, nil
} }
func GetOneWithFallback[T any](main string, defaultValue T, fn func(string) (T, error), names ...string) T {
v, _ := getOneWithFallback(main, names...)
value, err := fn(v)
if err != nil {
return defaultValue
}
return value
}
func getOneWithFallback(main string, names ...string) (string, string) { func getOneWithFallback(main string, names ...string) (string, string) {
value := GetOrFile(main) value := GetOrFile(main)
if value != "" { if len(value) > 0 {
return value, main return value, main
} }
for _, name := range names { for _, name := range names {
value := GetOrFile(name) value := GetOrFile(name)
if value != "" { if len(value) > 0 {
return value, main return value, main
} }
} }
@ -105,32 +94,43 @@ func getOneWithFallback(main string, names ...string) (string, string) {
return "", main return "", main
} }
// GetOrDefaultInt returns the given environment variable value as an integer.
// Returns the default if the envvar cannot be coopered to an int, or is not found.
func GetOrDefaultInt(envVar string, defaultValue int) int {
v, err := strconv.Atoi(GetOrFile(envVar))
if err != nil {
return defaultValue
}
return v
}
// GetOrDefaultSecond returns the given environment variable value as an time.Duration (second).
// Returns the default if the envvar cannot be coopered to an int, or is not found.
func GetOrDefaultSecond(envVar string, defaultValue time.Duration) time.Duration {
v := GetOrDefaultInt(envVar, -1)
if v < 0 {
return defaultValue
}
return time.Duration(v) * time.Second
}
// GetOrDefaultString returns the given environment variable value as a string. // GetOrDefaultString returns the given environment variable value as a string.
// Returns the default if the env var cannot be found. // Returns the default if the envvar cannot be find.
func GetOrDefaultString(envVar string, defaultValue string) string { func GetOrDefaultString(envVar, defaultValue string) string {
return getOrDefault(envVar, defaultValue, ParseString) v := GetOrFile(envVar)
if len(v) == 0 {
return defaultValue
}
return v
} }
// GetOrDefaultBool returns the given environment variable value as a boolean. // GetOrDefaultBool returns the given environment variable value as a boolean.
// Returns the default if the env var cannot be coopered to a boolean, or is not found. // Returns the default if the envvar cannot be coopered to a boolean, or is not found.
func GetOrDefaultBool(envVar string, defaultValue bool) bool { func GetOrDefaultBool(envVar string, defaultValue bool) bool {
return getOrDefault(envVar, defaultValue, strconv.ParseBool) v, err := strconv.ParseBool(GetOrFile(envVar))
}
// GetOrDefaultInt returns the given environment variable value as an integer.
// Returns the default if the env var cannot be coopered to an int, or is not found.
func GetOrDefaultInt(envVar string, defaultValue int) int {
return getOrDefault(envVar, defaultValue, strconv.Atoi)
}
// GetOrDefaultSecond returns the given environment variable value as a time.Duration (second).
// Returns the default if the env var cannot be coopered to an int, or is not found.
func GetOrDefaultSecond(envVar string, defaultValue time.Duration) time.Duration {
return getOrDefault(envVar, defaultValue, ParseSecond)
}
func getOrDefault[T any](envVar string, defaultValue T, fn func(string) (T, error)) T {
v, err := fn(GetOrFile(envVar))
if err != nil { if err != nil {
return defaultValue return defaultValue
} }
@ -153,7 +153,7 @@ func GetOrFile(envVar string) string {
return envVarValue return envVarValue
} }
fileContents, err := os.ReadFile(fileVarValue) fileContents, err := ioutil.ReadFile(fileVarValue)
if err != nil { if err != nil {
log.Printf("Failed to read the file %s (defined by env var %s): %s", fileVarValue, fileVar, err) log.Printf("Failed to read the file %s (defined by env var %s): %s", fileVarValue, fileVar, err)
return "" return ""
@ -161,26 +161,3 @@ func GetOrFile(envVar string) string {
return strings.TrimSuffix(string(fileContents), "\n") return strings.TrimSuffix(string(fileContents), "\n")
} }
// ParseSecond parses env var value (string) to a second (time.Duration).
func ParseSecond(s string) (time.Duration, error) {
v, err := strconv.Atoi(s)
if err != nil {
return 0, err
}
if v < 0 {
return 0, fmt.Errorf("unsupported value: %d", v)
}
return time.Duration(v) * time.Second, nil
}
// ParseString parses env var value (string) to a string but throws an error when the string is empty.
func ParseString(s string) (string, error) {
if s == "" {
return "", errors.New("empty string")
}
return s, nil
}

View File

@ -16,10 +16,7 @@ func For(msg string, timeout, interval time.Duration, f func() (bool, error)) er
for { for {
select { select {
case <-timeUp: case <-timeUp:
if lastErr == nil { return fmt.Errorf("time limit exceeded: last error: %w", lastErr)
return fmt.Errorf("%s: time limit exceeded", msg)
}
return fmt.Errorf("%s: time limit exceeded: last error: %w", msg, lastErr)
default: default:
} }

View File

@ -1,133 +0,0 @@
package errutils
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"strconv"
)
const legoDebugClientVerboseError = "LEGO_DEBUG_CLIENT_VERBOSE_ERROR"
// HTTPDoError uses with `(http.Client).Do` error.
type HTTPDoError struct {
req *http.Request
err error
}
// NewHTTPDoError creates a new HTTPDoError.
func NewHTTPDoError(req *http.Request, err error) *HTTPDoError {
return &HTTPDoError{req: req, err: err}
}
func (h HTTPDoError) Error() string {
msg := "unable to communicate with the API server:"
if ok, _ := strconv.ParseBool(os.Getenv(legoDebugClientVerboseError)); ok {
msg += fmt.Sprintf(" [request: %s %s]", h.req.Method, h.req.URL)
}
if h.err == nil {
return msg
}
return msg + fmt.Sprintf(" error: %v", h.err)
}
func (h HTTPDoError) Unwrap() error {
return h.err
}
// ReadResponseError use with `io.ReadAll` when reading response body.
type ReadResponseError struct {
req *http.Request
StatusCode int
err error
}
// NewReadResponseError creates a new ReadResponseError.
func NewReadResponseError(req *http.Request, statusCode int, err error) *ReadResponseError {
return &ReadResponseError{req: req, StatusCode: statusCode, err: err}
}
func (r ReadResponseError) Error() string {
msg := "unable to read response body:"
if ok, _ := strconv.ParseBool(os.Getenv(legoDebugClientVerboseError)); ok {
msg += fmt.Sprintf(" [request: %s %s]", r.req.Method, r.req.URL)
}
msg += fmt.Sprintf(" [status code: %d]", r.StatusCode)
if r.err == nil {
return msg
}
return msg + fmt.Sprintf(" error: %v", r.err)
}
func (r ReadResponseError) Unwrap() error {
return r.err
}
// UnmarshalError uses with `json.Unmarshal` or `xml.Unmarshal` when reading response body.
type UnmarshalError struct {
req *http.Request
StatusCode int
Body []byte
err error
}
// NewUnmarshalError creates a new UnmarshalError.
func NewUnmarshalError(req *http.Request, statusCode int, body []byte, err error) *UnmarshalError {
return &UnmarshalError{req: req, StatusCode: statusCode, Body: bytes.TrimSpace(body), err: err}
}
func (u UnmarshalError) Error() string {
msg := "unable to unmarshal response:"
if ok, _ := strconv.ParseBool(os.Getenv(legoDebugClientVerboseError)); ok {
msg += fmt.Sprintf(" [request: %s %s]", u.req.Method, u.req.URL)
}
msg += fmt.Sprintf(" [status code: %d] body: %s", u.StatusCode, string(u.Body))
if u.err == nil {
return msg
}
return msg + fmt.Sprintf(" error: %v", u.err)
}
func (u UnmarshalError) Unwrap() error {
return u.err
}
// UnexpectedStatusCodeError use when the status of the response is unexpected but there is no API error type.
type UnexpectedStatusCodeError struct {
req *http.Request
StatusCode int
Body []byte
}
// NewUnexpectedStatusCodeError creates a new UnexpectedStatusCodeError.
func NewUnexpectedStatusCodeError(req *http.Request, statusCode int, body []byte) *UnexpectedStatusCodeError {
return &UnexpectedStatusCodeError{req: req, StatusCode: statusCode, Body: bytes.TrimSpace(body)}
}
func NewUnexpectedResponseStatusCodeError(req *http.Request, resp *http.Response) *UnexpectedStatusCodeError {
raw, _ := io.ReadAll(resp.Body)
return &UnexpectedStatusCodeError{req: req, StatusCode: resp.StatusCode, Body: bytes.TrimSpace(raw)}
}
func (u UnexpectedStatusCodeError) Error() string {
msg := "unexpected status code:"
if ok, _ := strconv.ParseBool(os.Getenv(legoDebugClientVerboseError)); ok {
msg += fmt.Sprintf(" [request: %s %s]", u.req.Method, u.req.URL)
}
return msg + fmt.Sprintf(" [status code: %d] body: %s", u.StatusCode, string(u.Body))
}

View File

@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"strings"
"sync" "sync"
"time" "time"
@ -14,14 +15,16 @@ import (
) )
// OVH API reference: https://eu.api.ovh.com/ // OVH API reference: https://eu.api.ovh.com/
// Create a Token: https://eu.api.ovh.com/createToken/ // Create a Token: https://eu.api.ovh.com/createToken/
// Create a OAuth2 client: https://eu.api.ovh.com/console-preview/?section=%2Fme&branch=v1#post-/me/api/oauth2/client
// Environment variables names. // Environment variables names.
const ( const (
envNamespace = "OVH_" envNamespace = "OVH_"
EnvEndpoint = envNamespace + "ENDPOINT" EnvEndpoint = envNamespace + "ENDPOINT"
EnvApplicationKey = envNamespace + "APPLICATION_KEY"
EnvApplicationSecret = envNamespace + "APPLICATION_SECRET"
EnvConsumerKey = envNamespace + "CONSUMER_KEY"
EnvTTL = envNamespace + "TTL" EnvTTL = envNamespace + "TTL"
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
@ -29,19 +32,6 @@ const (
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
) )
// Authenticate using application key.
const (
EnvApplicationKey = envNamespace + "APPLICATION_KEY"
EnvApplicationSecret = envNamespace + "APPLICATION_SECRET"
EnvConsumerKey = envNamespace + "CONSUMER_KEY"
)
// Authenticate using OAuth2 client.
const (
EnvClientID = envNamespace + "CLIENT_ID"
EnvClientSecret = envNamespace + "CLIENT_SECRET"
)
// Record a DNS record. // Record a DNS record.
type Record struct { type Record struct {
ID int64 `json:"id,omitempty"` ID int64 `json:"id,omitempty"`
@ -52,32 +42,18 @@ type Record struct {
Zone string `json:"zone,omitempty"` Zone string `json:"zone,omitempty"`
} }
// OAuth2Config the OAuth2 specific configuration.
type OAuth2Config struct {
ClientID string
ClientSecret string
}
// Config is used to configure the creation of the DNSProvider. // Config is used to configure the creation of the DNSProvider.
type Config struct { type Config struct {
APIEndpoint string APIEndpoint string
ApplicationKey string
ApplicationKey string ApplicationSecret string
ApplicationSecret string ConsumerKey string
ConsumerKey string
OAuth2Config *OAuth2Config
PropagationTimeout time.Duration PropagationTimeout time.Duration
PollingInterval time.Duration PollingInterval time.Duration
TTL int TTL int
HTTPClient *http.Client HTTPClient *http.Client
} }
func (c *Config) hasAppKeyAuth() bool {
return c.ApplicationKey != "" || c.ApplicationSecret != "" || c.ConsumerKey != ""
}
// NewDefaultConfig returns a default configuration for the DNSProvider. // NewDefaultConfig returns a default configuration for the DNSProvider.
func NewDefaultConfig() *Config { func NewDefaultConfig() *Config {
return &Config{ return &Config{
@ -102,11 +78,17 @@ type DNSProvider struct {
// Credentials must be passed in the environment variables: // Credentials must be passed in the environment variables:
// OVH_ENDPOINT (must be either "ovh-eu" or "ovh-ca"), OVH_APPLICATION_KEY, OVH_APPLICATION_SECRET, OVH_CONSUMER_KEY. // OVH_ENDPOINT (must be either "ovh-eu" or "ovh-ca"), OVH_APPLICATION_KEY, OVH_APPLICATION_SECRET, OVH_CONSUMER_KEY.
func NewDNSProvider() (*DNSProvider, error) { func NewDNSProvider() (*DNSProvider, error) {
config, err := createConfigFromEnvVars() values, err := env.Get(EnvEndpoint, EnvApplicationKey, EnvApplicationSecret, EnvConsumerKey)
if err != nil { if err != nil {
return nil, fmt.Errorf("ovh: %w", err) return nil, fmt.Errorf("ovh: %w", err)
} }
config := NewDefaultConfig()
config.APIEndpoint = values[EnvEndpoint]
config.ApplicationKey = values[EnvApplicationKey]
config.ApplicationSecret = values[EnvApplicationSecret]
config.ConsumerKey = values[EnvConsumerKey]
return NewDNSProviderConfig(config) return NewDNSProviderConfig(config)
} }
@ -116,11 +98,16 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
return nil, errors.New("ovh: the configuration of the DNS provider is nil") return nil, errors.New("ovh: the configuration of the DNS provider is nil")
} }
if config.OAuth2Config != nil && config.hasAppKeyAuth() { if config.APIEndpoint == "" || config.ApplicationKey == "" || config.ApplicationSecret == "" || config.ConsumerKey == "" {
return nil, errors.New("ovh: can't use both authentication systems (ApplicationKey and OAuth2)") return nil, errors.New("ovh: credentials missing")
} }
client, err := newClient(config) client, err := ovh.NewClient(
config.APIEndpoint,
config.ApplicationKey,
config.ApplicationSecret,
config.ConsumerKey,
)
if err != nil { if err != nil {
return nil, fmt.Errorf("ovh: %w", err) return nil, fmt.Errorf("ovh: %w", err)
} }
@ -136,23 +123,19 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
// Present creates a TXT record to fulfill the dns-01 challenge. // Present creates a TXT record to fulfill the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error { func (d *DNSProvider) Present(domain, token, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth) fqdn, value := dns01.GetRecord(domain, keyAuth)
// Parse domain name // Parse domain name
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain))
if err != nil { if err != nil {
return fmt.Errorf("ovh: could not find zone for domain %q: %w", domain, err) return fmt.Errorf("ovh: could not determine zone for domain %q: %w", domain, err)
} }
authZone = dns01.UnFqdn(authZone) authZone = dns01.UnFqdn(authZone)
subDomain := extractRecordName(fqdn, authZone)
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
if err != nil {
return fmt.Errorf("ovh: %w", err)
}
reqURL := fmt.Sprintf("/domain/zone/%s/record", authZone) reqURL := fmt.Sprintf("/domain/zone/%s/record", authZone)
reqData := Record{FieldType: "TXT", SubDomain: subDomain, Target: info.Value, TTL: d.config.TTL} reqData := Record{FieldType: "TXT", SubDomain: subDomain, Target: value, TTL: d.config.TTL}
// Create TXT record // Create TXT record
var respData Record var respData Record
@ -177,19 +160,19 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
// CleanUp removes the TXT record matching the specified parameters. // CleanUp removes the TXT record matching the specified parameters.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth) fqdn, _ := dns01.GetRecord(domain, keyAuth)
// get the record's unique ID from when we created it // get the record's unique ID from when we created it
d.recordIDsMu.Lock() d.recordIDsMu.Lock()
recordID, ok := d.recordIDs[token] recordID, ok := d.recordIDs[token]
d.recordIDsMu.Unlock() d.recordIDsMu.Unlock()
if !ok { if !ok {
return fmt.Errorf("ovh: unknown record ID for '%s'", info.EffectiveFQDN) return fmt.Errorf("ovh: unknown record ID for '%s'", fqdn)
} }
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain))
if err != nil { if err != nil {
return fmt.Errorf("ovh: could not find zone for domain %q: %w", domain, err) return fmt.Errorf("ovh: could not determine zone for domain %q: %w", domain, err)
} }
authZone = dns01.UnFqdn(authZone) authZone = dns01.UnFqdn(authZone)
@ -222,94 +205,10 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval return d.config.PropagationTimeout, d.config.PollingInterval
} }
func createConfigFromEnvVars() (*Config, error) { func extractRecordName(fqdn, zone string) string {
firstAppKeyEnvVar := findFirstValuedEnvVar(EnvApplicationKey, EnvApplicationSecret, EnvConsumerKey) name := dns01.UnFqdn(fqdn)
firstOAuth2EnvVar := findFirstValuedEnvVar(EnvClientID, EnvClientSecret) if idx := strings.Index(name, "."+zone); idx != -1 {
return name[:idx]
if firstAppKeyEnvVar != "" && firstOAuth2EnvVar != "" {
return nil, fmt.Errorf("can't use both %s and %s at the same time", firstAppKeyEnvVar, firstOAuth2EnvVar)
} }
return name
config := NewDefaultConfig()
if firstOAuth2EnvVar != "" {
values, err := env.Get(EnvEndpoint, EnvClientID, EnvClientSecret)
if err != nil {
return nil, err
}
config.APIEndpoint = values[EnvEndpoint]
config.OAuth2Config = &OAuth2Config{
ClientID: values[EnvClientID],
ClientSecret: values[EnvClientSecret],
}
return config, nil
}
values, err := env.Get(EnvEndpoint, EnvApplicationKey, EnvApplicationSecret, EnvConsumerKey)
if err != nil {
return nil, err
}
config.APIEndpoint = values[EnvEndpoint]
config.ApplicationKey = values[EnvApplicationKey]
config.ApplicationSecret = values[EnvApplicationSecret]
config.ConsumerKey = values[EnvConsumerKey]
return config, nil
}
func findFirstValuedEnvVar(envVars ...string) string {
for _, envVar := range envVars {
if env.GetOrFile(envVar) != "" {
return envVar
}
}
return ""
}
func newClient(config *Config) (*ovh.Client, error) {
if config.OAuth2Config == nil {
return newClientApplicationKey(config)
}
return newClientOAuth2(config)
}
func newClientApplicationKey(config *Config) (*ovh.Client, error) {
if config.APIEndpoint == "" || config.ApplicationKey == "" || config.ApplicationSecret == "" || config.ConsumerKey == "" {
return nil, errors.New("credentials are missing")
}
client, err := ovh.NewClient(
config.APIEndpoint,
config.ApplicationKey,
config.ApplicationSecret,
config.ConsumerKey,
)
if err != nil {
return nil, fmt.Errorf("new client: %w", err)
}
return client, nil
}
func newClientOAuth2(config *Config) (*ovh.Client, error) {
if config.APIEndpoint == "" || config.OAuth2Config.ClientID == "" || config.OAuth2Config.ClientSecret == "" {
return nil, errors.New("credentials are missing")
}
client, err := ovh.NewOAuth2Client(
config.APIEndpoint,
config.OAuth2Config.ClientID,
config.OAuth2Config.ClientSecret,
)
if err != nil {
return nil, fmt.Errorf("new OAuth2 client: %w", err)
}
return client, nil
} }

View File

@ -5,20 +5,11 @@ Code = "ovh"
Since = "v0.4.0" Since = "v0.4.0"
Example = ''' Example = '''
# Application Key authentication:
OVH_APPLICATION_KEY=1234567898765432 \ OVH_APPLICATION_KEY=1234567898765432 \
OVH_APPLICATION_SECRET=b9841238feb177a84330febba8a832089 \ OVH_APPLICATION_SECRET=b9841238feb177a84330febba8a832089 \
OVH_CONSUMER_KEY=256vfsd347245sdfg \ OVH_CONSUMER_KEY=256vfsd347245sdfg \
OVH_ENDPOINT=ovh-eu \ OVH_ENDPOINT=ovh-eu \
lego --email you@example.com --dns ovh --domains my.example.org run lego --dns autodns --domains my.domain.com --email my@email.com run
# Or OAuth2:
OVH_CLIENT_ID=yyy \
OVH_CLIENT_SECRET=xxx \
OVH_ENDPOINT=ovh-eu \
lego --email you@example.com --dns ovh --domains my.example.org run
''' '''
Additional = ''' Additional = '''
@ -26,7 +17,7 @@ Additional = '''
Application key and secret can be created by following the [OVH guide](https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/). Application key and secret can be created by following the [OVH guide](https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/).
When requesting the consumer key, the following configuration can be used to define access rights: When requesting the consumer key, the following configuration can be use to define access rights:
```json ```json
{ {
@ -42,32 +33,14 @@ When requesting the consumer key, the following configuration can be used to def
] ]
} }
``` ```
## OAuth2 Client Credentials
Another method for authentication is by using OAuth2 client credentials.
An IAM policy and service account can be created by following the [OVH guide](https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343).
Following IAM policies need to be authorized for the affected domain:
* dnsZone:apiovh:record/create
* dnsZone:apiovh:record/delete
* dnsZone:apiovh:refresh
## Important Note
Both authentication methods cannot be used at the same time.
''' '''
[Configuration] [Configuration]
[Configuration.Credentials] [Configuration.Credentials]
OVH_ENDPOINT = "Endpoint URL (ovh-eu or ovh-ca)" OVH_ENDPOINT = "Endpoint URL (ovh-eu or ovh-ca)"
OVH_APPLICATION_KEY = "Application key (Application Key authentication)" OVH_APPLICATION_KEY = "Application key"
OVH_APPLICATION_SECRET = "Application secret (Application Key authentication)" OVH_APPLICATION_SECRET = "Application secret"
OVH_CONSUMER_KEY = "Consumer key (Application Key authentication)" OVH_CONSUMER_KEY = "Consumer key"
OVH_CLIENT_ID = "Client ID (OAuth2)"
OVH_CLIENT_SECRET = "Client secret (OAuth2)"
[Configuration.Additional] [Configuration.Additional]
OVH_POLLING_INTERVAL = "Time between DNS propagation check" OVH_POLLING_INTERVAL = "Time between DNS propagation check"
OVH_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" OVH_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"

View File

@ -1,228 +0,0 @@
package internal
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"path"
"strconv"
"strings"
"time"
"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
"github.com/miekg/dns"
)
// Client the PowerDNS API client.
type Client struct {
serverName string
apiKey string
apiVersion int
Host *url.URL
HTTPClient *http.Client
}
// NewClient creates a new Client.
func NewClient(host *url.URL, serverName string, apiVersion int, apiKey string) *Client {
return &Client{
serverName: serverName,
apiKey: apiKey,
apiVersion: apiVersion,
Host: host,
HTTPClient: &http.Client{Timeout: 5 * time.Second},
}
}
func (c *Client) APIVersion() int {
return c.apiVersion
}
func (c *Client) SetAPIVersion(ctx context.Context) error {
var err error
c.apiVersion, err = c.getAPIVersion(ctx)
return err
}
func (c *Client) getAPIVersion(ctx context.Context) (int, error) {
endpoint := c.joinPath("/", "api")
req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return 0, err
}
result, err := c.do(req)
if err != nil {
return 0, err
}
var versions []apiVersion
err = json.Unmarshal(result, &versions)
if err != nil {
return 0, err
}
latestVersion := 0
for _, v := range versions {
if v.Version > latestVersion {
latestVersion = v.Version
}
}
return latestVersion, err
}
func (c *Client) GetHostedZone(ctx context.Context, authZone string) (*HostedZone, error) {
endpoint := c.joinPath("/", "servers", c.serverName, "zones", dns.Fqdn(authZone))
req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
result, err := c.do(req)
if err != nil {
return nil, err
}
var zone HostedZone
err = json.Unmarshal(result, &zone)
if err != nil {
return nil, err
}
// convert pre-v1 API result
if len(zone.Records) > 0 {
zone.RRSets = []RRSet{}
for _, record := range zone.Records {
set := RRSet{
Name: record.Name,
Type: record.Type,
Records: []Record{record},
}
zone.RRSets = append(zone.RRSets, set)
}
}
return &zone, nil
}
func (c *Client) UpdateRecords(ctx context.Context, zone *HostedZone, sets RRSets) error {
endpoint := c.joinPath("/", "servers", c.serverName, "zones", zone.ID)
req, err := newJSONRequest(ctx, http.MethodPatch, endpoint, sets)
if err != nil {
return err
}
_, err = c.do(req)
if err != nil {
return err
}
return nil
}
func (c *Client) Notify(ctx context.Context, zone *HostedZone) error {
if c.apiVersion < 1 || zone.Kind != "Master" && zone.Kind != "Slave" {
return nil
}
endpoint := c.joinPath("/", "servers", c.serverName, "zones", zone.ID, "notify")
req, err := newJSONRequest(ctx, http.MethodPut, endpoint, nil)
if err != nil {
return err
}
_, err = c.do(req)
if err != nil {
return err
}
return nil
}
func (c *Client) joinPath(elem ...string) *url.URL {
p := path.Join(elem...)
if p != "/api" && c.apiVersion > 0 && !strings.HasPrefix(p, "/api/v") {
p = path.Join("/api", "v"+strconv.Itoa(c.apiVersion), p)
}
return c.Host.JoinPath(p)
}
func (c *Client) do(req *http.Request) (json.RawMessage, error) {
req.Header.Set("X-API-Key", c.apiKey)
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, errutils.NewHTTPDoError(req, err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusUnprocessableEntity && (resp.StatusCode < 200 || resp.StatusCode >= 300) {
return nil, errutils.NewUnexpectedResponseStatusCodeError(req, resp)
}
var msg json.RawMessage
err = json.NewDecoder(resp.Body).Decode(&msg)
if err != nil {
if errors.Is(err, io.EOF) {
// empty body
return nil, nil
}
// other error
return nil, err
}
// check for PowerDNS error message
if len(msg) > 0 && msg[0] == '{' {
var errInfo apiError
err = json.Unmarshal(msg, &errInfo)
if err != nil {
return nil, errutils.NewUnmarshalError(req, resp.StatusCode, msg, err)
}
if errInfo.ShortMsg != "" {
return nil, fmt.Errorf("error talking to PDNS API: %w", errInfo)
}
}
return msg, nil
}
func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
buf := new(bytes.Buffer)
if payload != nil {
err := json.NewEncoder(buf).Encode(payload)
if err != nil {
return nil, fmt.Errorf("failed to create request JSON body: %w", err)
}
}
req, err := http.NewRequestWithContext(ctx, method, strings.TrimSuffix(endpoint.String(), "/"), buf)
if err != nil {
return nil, fmt.Errorf("unable to create request: %w", err)
}
req.Header.Set("Accept", "application/json")
// PowerDNS doesn't follow HTTP convention about the "Content-Type" header.
if method != http.MethodGet && method != http.MethodDelete {
req.Header.Set("Content-Type", "application/json")
}
return req, nil
}

View File

@ -1,48 +0,0 @@
package internal
type Record struct {
Content string `json:"content"`
Disabled bool `json:"disabled"`
// pre-v1 API
Name string `json:"name"`
Type string `json:"type"`
TTL int `json:"ttl,omitempty"`
}
type HostedZone struct {
ID string `json:"id"`
Name string `json:"name"`
URL string `json:"url"`
Kind string `json:"kind"`
RRSets []RRSet `json:"rrsets"`
// pre-v1 API
Records []Record `json:"records"`
}
type RRSet struct {
Name string `json:"name"`
Type string `json:"type"`
Kind string `json:"kind"`
ChangeType string `json:"changetype"`
Records []Record `json:"records,omitempty"`
TTL int `json:"ttl,omitempty"`
}
type RRSets struct {
RRSets []RRSet `json:"rrsets"`
}
type apiError struct {
ShortMsg string `json:"error"`
}
func (a apiError) Error() string {
return a.ShortMsg
}
type apiVersion struct {
URL string `json:"url"`
Version int `json:"version"`
}

View File

@ -1,228 +0,0 @@
// Package pdns implements a DNS provider for solving the DNS-01 challenge using PowerDNS nameserver.
package pdns
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"time"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/log"
"github.com/go-acme/lego/v4/platform/config/env"
"github.com/go-acme/lego/v4/providers/dns/pdns/internal"
)
// Environment variables names.
const (
envNamespace = "PDNS_"
EnvAPIKey = envNamespace + "API_KEY"
EnvAPIURL = envNamespace + "API_URL"
EnvTTL = envNamespace + "TTL"
EnvAPIVersion = envNamespace + "API_VERSION"
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
EnvServerName = envNamespace + "SERVER_NAME"
)
// Config is used to configure the creation of the DNSProvider.
type Config struct {
APIKey string
Host *url.URL
ServerName string
APIVersion int
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider.
func NewDefaultConfig() *Config {
return &Config{
ServerName: env.GetOrDefaultString(EnvServerName, "localhost"),
APIVersion: env.GetOrDefaultInt(EnvAPIVersion, 0),
TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
},
}
}
// DNSProvider implements the challenge.Provider interface.
type DNSProvider struct {
config *Config
client *internal.Client
}
// NewDNSProvider returns a DNSProvider instance configured for pdns.
// Credentials must be passed in the environment variable:
// PDNS_API_URL and PDNS_API_KEY.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get(EnvAPIKey, EnvAPIURL)
if err != nil {
return nil, fmt.Errorf("pdns: %w", err)
}
hostURL, err := url.Parse(values[EnvAPIURL])
if err != nil {
return nil, fmt.Errorf("pdns: %w", err)
}
config := NewDefaultConfig()
config.Host = hostURL
config.APIKey = values[EnvAPIKey]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for pdns.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("pdns: the configuration of the DNS provider is nil")
}
if config.APIKey == "" {
return nil, errors.New("pdns: API key missing")
}
if config.Host == nil || config.Host.Host == "" {
return nil, errors.New("pdns: API URL missing")
}
client := internal.NewClient(config.Host, config.ServerName, config.APIVersion, config.APIKey)
if config.APIVersion <= 0 {
err := client.SetAPIVersion(context.Background())
if err != nil {
log.Warnf("pdns: failed to get API version %v", err)
}
}
return &DNSProvider{config: config, client: client}, nil
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
// Present creates a TXT record to fulfill the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
if err != nil {
return fmt.Errorf("pdns: could not find zone for domain %q: %w", domain, err)
}
ctx := context.Background()
zone, err := d.client.GetHostedZone(ctx, authZone)
if err != nil {
return fmt.Errorf("pdns: %w", err)
}
name := info.EffectiveFQDN
if d.client.APIVersion() == 0 {
// pre-v1 API wants non-fqdn
name = dns01.UnFqdn(info.EffectiveFQDN)
}
// Look for existing records.
existingRRSet := findTxtRecord(zone, info.EffectiveFQDN)
// merge the existing and new records
var records []internal.Record
if existingRRSet != nil {
records = existingRRSet.Records
}
rec := internal.Record{
Content: "\"" + info.Value + "\"",
Disabled: false,
// pre-v1 API
Type: "TXT",
Name: name,
TTL: d.config.TTL,
}
rrSets := internal.RRSets{
RRSets: []internal.RRSet{
{
Name: name,
ChangeType: "REPLACE",
Type: "TXT",
Kind: "Master",
TTL: d.config.TTL,
Records: append(records, rec),
},
},
}
err = d.client.UpdateRecords(ctx, zone, rrSets)
if err != nil {
return fmt.Errorf("pdns: %w", err)
}
return d.client.Notify(ctx, zone)
}
// CleanUp removes the TXT record matching the specified parameters.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
if err != nil {
return fmt.Errorf("pdns: could not find zone for domain %q: %w", domain, err)
}
ctx := context.Background()
zone, err := d.client.GetHostedZone(ctx, authZone)
if err != nil {
return fmt.Errorf("pdns: %w", err)
}
set := findTxtRecord(zone, info.EffectiveFQDN)
if set == nil {
return fmt.Errorf("pdns: no existing record found for %s", info.EffectiveFQDN)
}
rrSets := internal.RRSets{
RRSets: []internal.RRSet{
{
Name: set.Name,
Type: set.Type,
ChangeType: "DELETE",
},
},
}
err = d.client.UpdateRecords(ctx, zone, rrSets)
if err != nil {
return fmt.Errorf("pdns: %w", err)
}
return d.client.Notify(ctx, zone)
}
func findTxtRecord(zone *internal.HostedZone, fqdn string) *internal.RRSet {
for _, set := range zone.RRSets {
if set.Type == "TXT" && (set.Name == dns01.UnFqdn(fqdn) || set.Name == fqdn) {
return &set
}
}
return nil
}

View File

@ -1,37 +0,0 @@
Name = "PowerDNS"
Description = ''''''
URL = "https://www.powerdns.com/"
Code = "pdns"
Since = "v0.4.0"
Example = '''
PDNS_API_URL=http://pdns-server:80/ \
PDNS_API_KEY=xxxx \
lego --email you@example.com --dns pdns --domains my.example.org run
'''
Additional = '''
## Information
Tested and confirmed to work with PowerDNS authoritative server 3.4.8 and 4.0.1. Refer to [PowerDNS documentation](https://doc.powerdns.com/md/httpapi/README/) instructions on how to enable the built-in API interface.
PowerDNS Notes:
- PowerDNS API does not currently support SSL, therefore you should take care to ensure that traffic between lego and the PowerDNS API is over a trusted network, VPN etc.
- In order to have the SOA serial automatically increment each time the `_acme-challenge` record is added/modified via the API, set `SOA-EDIT-API` to `INCEPTION-INCREMENT` for the zone in the `domainmetadata` table
- Some PowerDNS servers doesn't have root API endpoints enabled and API version autodetection will not work. In that case version number can be defined using `PDNS_API_VERSION`.
'''
[Configuration]
[Configuration.Credentials]
PDNS_API_KEY = "API key"
PDNS_API_URL = "API URL"
[Configuration.Additional]
PDNS_SERVER_NAME = "Name of the server in the URL, 'localhost' by default"
PDNS_API_VERSION = "Skip API version autodetection and use the provided version number."
PDNS_POLLING_INTERVAL = "Time between DNS propagation check"
PDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
PDNS_TTL = "The TTL of the TXT record used for the DNS challenge"
PDNS_HTTP_TIMEOUT = "API request timeout"
[Links]
API = "https://doc.powerdns.com/md/httpapi/README/"

View File

@ -9,11 +9,9 @@ import (
"github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/log"
) )
const mailTo = "mailto:"
// Resource represents all important information about a registration // Resource represents all important information about a registration
// of which the client needs to keep track itself. // of which the client needs to keep track itself.
// WARNING: will be removed in the future (acme.ExtendedAccount), https://github.com/go-acme/lego/issues/855. // WARNING: will be remove in the future (acme.ExtendedAccount), https://github.com/go-acme/lego/issues/855.
type Resource struct { type Resource struct {
Body acme.Account `json:"body,omitempty"` Body acme.Account `json:"body,omitempty"`
URI string `json:"uri,omitempty"` URI string `json:"uri,omitempty"`
@ -54,12 +52,12 @@ func (r *Registrar) Register(options RegisterOptions) (*Resource, error) {
if r.user.GetEmail() != "" { if r.user.GetEmail() != "" {
log.Infof("acme: Registering account for %s", r.user.GetEmail()) log.Infof("acme: Registering account for %s", r.user.GetEmail())
accMsg.Contact = []string{mailTo + r.user.GetEmail()} accMsg.Contact = []string{"mailto:" + r.user.GetEmail()}
} }
account, err := r.core.Accounts.New(accMsg) account, err := r.core.Accounts.New(accMsg)
if err != nil { if err != nil {
// seems impossible // FIXME seems impossible
var errorDetails acme.ProblemDetails var errorDetails acme.ProblemDetails
if !errors.As(err, &errorDetails) || errorDetails.HTTPStatus != http.StatusConflict { if !errors.As(err, &errorDetails) || errorDetails.HTTPStatus != http.StatusConflict {
return nil, err return nil, err
@ -78,12 +76,12 @@ func (r *Registrar) RegisterWithExternalAccountBinding(options RegisterEABOption
if r.user.GetEmail() != "" { if r.user.GetEmail() != "" {
log.Infof("acme: Registering account for %s", r.user.GetEmail()) log.Infof("acme: Registering account for %s", r.user.GetEmail())
accMsg.Contact = []string{mailTo + r.user.GetEmail()} accMsg.Contact = []string{"mailto:" + r.user.GetEmail()}
} }
account, err := r.core.Accounts.NewEAB(accMsg, options.Kid, options.HmacEncoded) account, err := r.core.Accounts.NewEAB(accMsg, options.Kid, options.HmacEncoded)
if err != nil { if err != nil {
// seems impossible // FIXME seems impossible
var errorDetails acme.ProblemDetails var errorDetails acme.ProblemDetails
if !errors.As(err, &errorDetails) || errorDetails.HTTPStatus != http.StatusConflict { if !errors.As(err, &errorDetails) || errorDetails.HTTPStatus != http.StatusConflict {
return nil, err return nil, err
@ -130,7 +128,7 @@ func (r *Registrar) UpdateRegistration(options RegisterOptions) (*Resource, erro
if r.user.GetEmail() != "" { if r.user.GetEmail() != "" {
log.Infof("acme: Registering account for %s", r.user.GetEmail()) log.Infof("acme: Registering account for %s", r.user.GetEmail())
accMsg.Contact = []string{mailTo + r.user.GetEmail()} accMsg.Contact = []string{"mailto:" + r.user.GetEmail()}
} }
accountURL := r.user.GetRegistration().URI accountURL := r.user.GetRegistration().URI

View File

@ -1,2 +0,0 @@
jose-util/jose-util
jose-util.t.err

View File

@ -1,53 +0,0 @@
# https://github.com/golangci/golangci-lint
run:
skip-files:
- doc_test.go
modules-download-mode: readonly
linters:
enable-all: true
disable:
- gochecknoglobals
- goconst
- lll
- maligned
- nakedret
- scopelint
- unparam
- funlen # added in 1.18 (requires go-jose changes before it can be enabled)
linters-settings:
gocyclo:
min-complexity: 35
issues:
exclude-rules:
- text: "don't use ALL_CAPS in Go names"
linters:
- golint
- text: "hardcoded credentials"
linters:
- gosec
- text: "weak cryptographic primitive"
linters:
- gosec
- path: json/
linters:
- dupl
- errcheck
- gocritic
- gocyclo
- golint
- govet
- ineffassign
- staticcheck
- structcheck
- stylecheck
- unused
- path: _test\.go
linters:
- scopelint
- path: jwk.go
linters:
- gocyclo

View File

@ -1,33 +0,0 @@
language: go
matrix:
fast_finish: true
allow_failures:
- go: tip
go:
- "1.13.x"
- "1.14.x"
- tip
before_script:
- export PATH=$HOME/.local/bin:$PATH
before_install:
- go get -u github.com/mattn/goveralls github.com/wadey/gocovmerge
- curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.18.0
- pip install cram --user
script:
- go test -v -covermode=count -coverprofile=profile.cov .
- go test -v -covermode=count -coverprofile=cryptosigner/profile.cov ./cryptosigner
- go test -v -covermode=count -coverprofile=cipher/profile.cov ./cipher
- go test -v -covermode=count -coverprofile=jwt/profile.cov ./jwt
- go test -v ./json # no coverage for forked encoding/json package
- golangci-lint run
- cd jose-util && go build && PATH=$PWD:$PATH cram -v jose-util.t # cram tests jose-util
- cd ..
after_success:
- gocovmerge *.cov */*.cov > merged.coverprofile
- goveralls -coverprofile merged.coverprofile -service=travis-ci

View File

@ -1,96 +0,0 @@
# v4.0.4
## Fixed
- Reverted "Allow unmarshalling JSONWebKeySets with unsupported key types" as a
breaking change. See #136 / #137.
# v4.0.3
## Changed
- Allow unmarshalling JSONWebKeySets with unsupported key types (#130)
- Document that OpaqueKeyEncrypter can't be implemented (for now) (#129)
- Dependency updates
# v4.0.2
## Changed
- Improved documentation of Verify() to note that JSONWebKeySet is a supported
argument type (#104)
- Defined exported error values for missing x5c header and unsupported elliptic
curves error cases (#117)
# v4.0.1
## Fixed
- An attacker could send a JWE containing compressed data that used large
amounts of memory and CPU when decompressed by `Decrypt` or `DecryptMulti`.
Those functions now return an error if the decompressed data would exceed
250kB or 10x the compressed size (whichever is larger). Thanks to
Enze Wang@Alioth and Jianjun Chen@Zhongguancun Lab (@zer0yu and @chenjj)
for reporting.
# v4.0.0
This release makes some breaking changes in order to more thoroughly
address the vulnerabilities discussed in [Three New Attacks Against JSON Web
Tokens][1], "Sign/encrypt confusion", "Billion hash attack", and "Polyglot
token".
## Changed
- Limit JWT encryption types (exclude password or public key types) (#78)
- Enforce minimum length for HMAC keys (#85)
- jwt: match any audience in a list, rather than requiring all audiences (#81)
- jwt: accept only Compact Serialization (#75)
- jws: Add expected algorithms for signatures (#74)
- Require specifying expected algorithms for ParseEncrypted,
ParseSigned, ParseDetached, jwt.ParseEncrypted, jwt.ParseSigned,
jwt.ParseSignedAndEncrypted (#69, #74)
- Usually there is a small, known set of appropriate algorithms for a program
to use and it's a mistake to allow unexpected algorithms. For instance the
"billion hash attack" relies in part on programs accepting the PBES2
encryption algorithm and doing the necessary work even if they weren't
specifically configured to allow PBES2.
- Revert "Strip padding off base64 strings" (#82)
- The specs require base64url encoding without padding.
- Minimum supported Go version is now 1.21
## Added
- ParseSignedCompact, ParseSignedJSON, ParseEncryptedCompact, ParseEncryptedJSON.
- These allow parsing a specific serialization, as opposed to ParseSigned and
ParseEncrypted, which try to automatically detect which serialization was
provided. It's common to require a specific serialization for a specific
protocol - for instance JWT requires Compact serialization.
[1]: https://i.blackhat.com/BH-US-23/Presentations/US-23-Tervoort-Three-New-Attacks-Against-JSON-Web-Tokens.pdf
# v3.0.2
## Fixed
- DecryptMulti: handle decompression error (#19)
## Changed
- jwe/CompactSerialize: improve performance (#67)
- Increase the default number of PBKDF2 iterations to 600k (#48)
- Return the proper algorithm for ECDSA keys (#45)
## Added
- Add Thumbprint support for opaque signers (#38)
# v3.0.1
## Fixed
- Security issue: an attacker specifying a large "p2c" value can cause
JSONWebEncryption.Decrypt and JSONWebEncryption.DecryptMulti to consume large
amounts of CPU, causing a DoS. Thanks to Matt Schwager (@mschwager) for the
disclosure and to Tom Tervoort for originally publishing the category of attack.
https://i.blackhat.com/BH-US-23/Presentations/US-23-Tervoort-Three-New-Attacks-Against-JSON-Web-Tokens.pdf

View File

@ -1,13 +0,0 @@
# Security Policy
This document explains how to contact the Let's Encrypt security team to report security vulnerabilities.
## Supported Versions
| Version | Supported |
| ------- | ----------|
| >= v3 | &check; |
| v2 | &cross; |
| v1 | &cross; |
## Reporting a vulnerability
Please see [https://letsencrypt.org/contact/#security](https://letsencrypt.org/contact/#security) for the email address to report a vulnerability. Ensure that the subject line for your report contains the word `vulnerability` and is descriptive. Your email should be acknowledged within 24 hours. If you do not receive a response within 24 hours, please follow-up again with another email.

View File

@ -1,32 +0,0 @@
codecov:
require_ci_to_pass: yes
coverage:
precision: 2
round: down
range: "70...100"
status:
project:
default:
target: 70%
threshold: 2%
patch: off
changes: no
parsers:
gcov:
branch_detection:
conditional: yes
loop: yes
method: no
macro: no
comment:
layout: "header,diff"
behavior: default
require_changes: no
ignore:
- internal/encoder/vm_color
- internal/encoder/vm_color_indent

View File

@ -1,2 +0,0 @@
cover.html
cover.out

View File

@ -1,86 +0,0 @@
run:
skip-files:
- encode_optype.go
- ".*_test\\.go$"
linters-settings:
govet:
enable-all: true
disable:
- shadow
linters:
enable-all: true
disable:
- dogsled
- dupl
- exhaustive
- exhaustivestruct
- errorlint
- forbidigo
- funlen
- gci
- gochecknoglobals
- gochecknoinits
- gocognit
- gocritic
- gocyclo
- godot
- godox
- goerr113
- gofumpt
- gomnd
- gosec
- ifshort
- lll
- makezero
- nakedret
- nestif
- nlreturn
- paralleltest
- testpackage
- thelper
- wrapcheck
- interfacer
- lll
- nakedret
- nestif
- nlreturn
- testpackage
- wsl
- varnamelen
- nilnil
- ireturn
- govet
- forcetypeassert
- cyclop
- containedctx
- revive
- nosnakecase
- exhaustruct
- depguard
issues:
exclude-rules:
# not needed
- path: /*.go
text: "ST1003: should not use underscores in package names"
linters:
- stylecheck
- path: /*.go
text: "don't use an underscore in package name"
linters:
- golint
- path: rtype.go
linters:
- golint
- stylecheck
- path: error.go
linters:
- staticcheck
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
max-issues-per-linter: 0
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
max-same-issues: 0

View File

@ -1,425 +0,0 @@
# v0.10.2 - 2023/03/20
### New features
* Support DebugDOT option for debugging encoder ( #440 )
### Fix bugs
* Fix combination of embedding structure and omitempty option ( #442 )
# v0.10.1 - 2023/03/13
### Fix bugs
* Fix checkptr error for array decoder ( #415 )
* Fix added buffer size check when decoding key ( #430 )
* Fix handling of anonymous fields other than struct ( #431 )
* Fix to not optimize when lower conversion can't handle byte-by-byte ( #432 )
* Fix a problem that MarshalIndent does not work when UnorderedMap is specified ( #435 )
* Fix mapDecoder.DecodeStream() for empty objects containing whitespace ( #425 )
* Fix an issue that could not set the correct NextField for fields in the embedded structure ( #438 )
# v0.10.0 - 2022/11/29
### New features
* Support JSON Path ( #250 )
### Fix bugs
* Fix marshaler for map's key ( #409 )
# v0.9.11 - 2022/08/18
### Fix bugs
* Fix unexpected behavior when buffer ends with backslash ( #383 )
* Fix stream decoding of escaped character ( #387 )
# v0.9.10 - 2022/07/15
### Fix bugs
* Fix boundary exception of type caching ( #382 )
# v0.9.9 - 2022/07/15
### Fix bugs
* Fix encoding of directed interface with typed nil ( #377 )
* Fix embedded primitive type encoding using alias ( #378 )
* Fix slice/array type encoding with types implementing MarshalJSON ( #379 )
* Fix unicode decoding when the expected buffer state is not met after reading ( #380 )
# v0.9.8 - 2022/06/30
### Fix bugs
* Fix decoding of surrogate-pair ( #365 )
* Fix handling of embedded primitive type ( #366 )
* Add validation of escape sequence for decoder ( #367 )
* Fix stream tokenizing respecting UseNumber ( #369 )
* Fix encoding when struct pointer type that implements Marshal JSON is embedded ( #375 )
### Improve performance
* Improve performance of linkRecursiveCode ( #368 )
# v0.9.7 - 2022/04/22
### Fix bugs
#### Encoder
* Add filtering process for encoding on slow path ( #355 )
* Fix encoding of interface{} with pointer type ( #363 )
#### Decoder
* Fix map key decoder that implements UnmarshalJSON ( #353 )
* Fix decoding of []uint8 type ( #361 )
### New features
* Add DebugWith option for encoder ( #356 )
# v0.9.6 - 2022/03/22
### Fix bugs
* Correct the handling of the minimum value of int type for decoder ( #344 )
* Fix bugs of stream decoder's bufferSize ( #349 )
* Add a guard to use typeptr more safely ( #351 )
### Improve decoder performance
* Improve escapeString's performance ( #345 )
### Others
* Update go version for CI ( #347 )
# v0.9.5 - 2022/03/04
### Fix bugs
* Fix panic when decoding time.Time with context ( #328 )
* Fix reading the next character in buffer to nul consideration ( #338 )
* Fix incorrect handling on skipValue ( #341 )
### Improve decoder performance
* Improve performance when a payload contains escape sequence ( #334 )
# v0.9.4 - 2022/01/21
* Fix IsNilForMarshaler for string type with omitempty ( #323 )
* Fix the case where the embedded field is at the end ( #326 )
# v0.9.3 - 2022/01/14
* Fix logic of removing struct field for decoder ( #322 )
# v0.9.2 - 2022/01/14
* Add invalid decoder to delay type error judgment at decode ( #321 )
# v0.9.1 - 2022/01/11
* Fix encoding of MarshalText/MarshalJSON operation with head offset ( #319 )
# v0.9.0 - 2022/01/05
### New feature
* Supports dynamic filtering of struct fields ( #314 )
### Improve encoding performance
* Improve map encoding performance ( #310 )
* Optimize encoding path for escaped string ( #311 )
* Add encoding option for performance ( #312 )
### Fix bugs
* Fix panic at encoding map value on 1.18 ( #310 )
* Fix MarshalIndent for interface type ( #317 )
# v0.8.1 - 2021/12/05
* Fix operation conversion from PtrHead to Head in Recursive type ( #305 )
# v0.8.0 - 2021/12/02
* Fix embedded field conflict behavior ( #300 )
* Refactor compiler for encoder ( #301 #302 )
# v0.7.10 - 2021/10/16
* Fix conversion from pointer to uint64 ( #294 )
# v0.7.9 - 2021/09/28
* Fix encoding of nil value about interface type that has method ( #291 )
# v0.7.8 - 2021/09/01
* Fix mapassign_faststr for indirect struct type ( #283 )
* Fix encoding of not empty interface type ( #284 )
* Fix encoding of empty struct interface type ( #286 )
# v0.7.7 - 2021/08/25
* Fix invalid utf8 on stream decoder ( #279 )
* Fix buffer length bug on string stream decoder ( #280 )
Thank you @orisano !!
# v0.7.6 - 2021/08/13
* Fix nil slice assignment ( #276 )
* Improve error message ( #277 )
# v0.7.5 - 2021/08/12
* Fix encoding of embedded struct with tags ( #265 )
* Fix encoding of embedded struct that isn't first field ( #272 )
* Fix decoding of binary type with escaped char ( #273 )
# v0.7.4 - 2021/07/06
* Fix encoding of indirect layout structure ( #264 )
# v0.7.3 - 2021/06/29
* Fix encoding of pointer type in empty interface ( #262 )
# v0.7.2 - 2021/06/26
### Fix decoder
* Add decoder for func type to fix decoding of nil function value ( #257 )
* Fix stream decoding of []byte type ( #258 )
### Performance
* Improve decoding performance of map[string]interface{} type ( use `mapassign_faststr` ) ( #256 )
* Improve encoding performance of empty interface type ( remove recursive calling of `vm.Run` ) ( #259 )
### Benchmark
* Add bytedance/sonic as benchmark target ( #254 )
# v0.7.1 - 2021/06/18
### Fix decoder
* Fix error when unmarshal empty array ( #253 )
# v0.7.0 - 2021/06/12
### Support context for MarshalJSON and UnmarshalJSON ( #248 )
* json.MarshalContext(context.Context, interface{}, ...json.EncodeOption) ([]byte, error)
* json.NewEncoder(io.Writer).EncodeContext(context.Context, interface{}, ...json.EncodeOption) error
* json.UnmarshalContext(context.Context, []byte, interface{}, ...json.DecodeOption) error
* json.NewDecoder(io.Reader).DecodeContext(context.Context, interface{}) error
```go
type MarshalerContext interface {
MarshalJSON(context.Context) ([]byte, error)
}
type UnmarshalerContext interface {
UnmarshalJSON(context.Context, []byte) error
}
```
### Add DecodeFieldPriorityFirstWin option ( #242 )
In the default behavior, go-json, like encoding/json, will reflect the result of the last evaluation when a field with the same name exists. I've added new options to allow you to change this behavior. `json.DecodeFieldPriorityFirstWin` option reflects the result of the first evaluation if a field with the same name exists. This behavior has a performance advantage as it allows the subsequent strings to be skipped if all fields have been evaluated.
### Fix encoder
* Fix indent number contains recursive type ( #249 )
* Fix encoding of using empty interface as map key ( #244 )
### Fix decoder
* Fix decoding fields containing escaped characters ( #237 )
### Refactor
* Move some tests to subdirectory ( #243 )
* Refactor package layout for decoder ( #238 )
# v0.6.1 - 2021/06/02
### Fix encoder
* Fix value of totalLength for encoding ( #236 )
# v0.6.0 - 2021/06/01
### Support Colorize option for encoding (#233)
```go
b, err := json.MarshalWithOption(v, json.Colorize(json.DefaultColorScheme))
if err != nil {
...
}
fmt.Println(string(b)) // print colored json
```
### Refactor
* Fix opcode layout - Adjust memory layout of the opcode to 128 bytes in a 64-bit environment ( #230 )
* Refactor encode option ( #231 )
* Refactor escape string ( #232 )
# v0.5.1 - 2021/5/20
### Optimization
* Add type addrShift to enable bigger encoder/decoder cache ( #213 )
### Fix decoder
* Keep original reference of slice element ( #229 )
### Refactor
* Refactor Debug mode for encoding ( #226 )
* Generate VM sources for encoding ( #227 )
* Refactor validator for null/true/false for decoding ( #221 )
# v0.5.0 - 2021/5/9
### Supports using omitempty and string tags at the same time ( #216 )
### Fix decoder
* Fix stream decoder for unicode char ( #215 )
* Fix decoding of slice element ( #219 )
* Fix calculating of buffer length for stream decoder ( #220 )
### Refactor
* replace skipWhiteSpace goto by loop ( #212 )
# v0.4.14 - 2021/5/4
### Benchmark
* Add valyala/fastjson to benchmark ( #193 )
* Add benchmark task for CI ( #211 )
### Fix decoder
* Fix decoding of slice with unmarshal json type ( #198 )
* Fix decoding of null value for interface type that does not implement Unmarshaler ( #205 )
* Fix decoding of null value to []byte by json.Unmarshal ( #206 )
* Fix decoding of backslash char at the end of string ( #207 )
* Fix stream decoder for null/true/false value ( #208 )
* Fix stream decoder for slow reader ( #211 )
### Performance
* If cap of slice is enough, reuse slice data for compatibility with encoding/json ( #200 )
# v0.4.13 - 2021/4/20
### Fix json.Compact and json.Indent
* Support validation the input buffer for json.Compact and json.Indent ( #189 )
* Optimize json.Compact and json.Indent ( improve memory footprint ) ( #190 )
# v0.4.12 - 2021/4/15
### Fix encoder
* Fix unnecessary indent for empty slice type ( #181 )
* Fix encoding of omitempty feature for the slice or interface type ( #183 )
* Fix encoding custom types zero values with omitempty when marshaller exists ( #187 )
### Fix decoder
* Fix decoder for invalid top level value ( #184 )
* Fix decoder for invalid number value ( #185 )
# v0.4.11 - 2021/4/3
* Improve decoder performance for interface type
# v0.4.10 - 2021/4/2
### Fix encoder
* Fixed a bug when encoding slice and map containing recursive structures
* Fixed a logic to determine if indirect reference
# v0.4.9 - 2021/3/29
### Add debug mode
If you use `json.MarshalWithOption(v, json.Debug())` and `panic` occurred in `go-json`, produces debug information to console.
### Support a new feature to compatible with encoding/json
- invalid UTF-8 is coerced to valid UTF-8 ( without performance down )
### Fix encoder
- Fixed handling of MarshalJSON of function type
### Fix decoding of slice of pointer type
If there is a pointer value, go-json will use it. (This behavior is necessary to achieve the ability to prioritize pre-filled values). However, since slices are reused internally, there was a bug that referred to the previous pointer value. Therefore, it is not necessary to refer to the pointer value in advance for the slice element, so we explicitly initialize slice element by `nil`.
# v0.4.8 - 2021/3/21
### Reduce memory usage at compile time
* go-json have used about 2GB of memory at compile time, but now it can compile with about less than 550MB.
### Fix any encoder's bug
* Add many test cases for encoder
* Fix composite type ( slice/array/map )
* Fix pointer types
* Fix encoding of MarshalJSON or MarshalText or json.Number type
### Refactor encoder
* Change package layout for reducing memory usage at compile
* Remove anonymous and only operation
* Remove root property from encodeCompileContext and opcode
### Fix CI
* Add Go 1.16
* Remove Go 1.13
* Fix `make cover` task
### Number/Delim/Token/RawMessage use the types defined in encoding/json by type alias
# v0.4.7 - 2021/02/22
### Fix decoder
* Fix decoding of deep recursive structure
* Fix decoding of embedded unexported pointer field
* Fix invalid test case
* Fix decoding of invalid value
* Fix decoding of prefilled value
* Fix not being able to return UnmarshalTypeError when it should be returned
* Fix decoding of null value
* Fix decoding of type of null string
* Use pre allocated pointer if exists it at decoding
### Reduce memory usage at compile
* Integrate int/int8/int16/int32/int64 and uint/uint8/uint16/uint32/uint64 operation to reduce memory usage at compile
### Remove unnecessary optype

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2020 Masaaki Goshima
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,39 +0,0 @@
PKG := github.com/goccy/go-json
BIN_DIR := $(CURDIR)/bin
PKGS := $(shell go list ./... | grep -v internal/cmd|grep -v test)
COVER_PKGS := $(foreach pkg,$(PKGS),$(subst $(PKG),.,$(pkg)))
COMMA := ,
EMPTY :=
SPACE := $(EMPTY) $(EMPTY)
COVERPKG_OPT := $(subst $(SPACE),$(COMMA),$(COVER_PKGS))
$(BIN_DIR):
@mkdir -p $(BIN_DIR)
.PHONY: cover
cover:
go test -coverpkg=$(COVERPKG_OPT) -coverprofile=cover.out ./...
.PHONY: cover-html
cover-html: cover
go tool cover -html=cover.out
.PHONY: lint
lint: golangci-lint
$(BIN_DIR)/golangci-lint run
golangci-lint: | $(BIN_DIR)
@{ \
set -e; \
GOLANGCI_LINT_TMP_DIR=$$(mktemp -d); \
cd $$GOLANGCI_LINT_TMP_DIR; \
go mod init tmp; \
GOBIN=$(BIN_DIR) go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.54.2; \
rm -rf $$GOLANGCI_LINT_TMP_DIR; \
}
.PHONY: generate
generate:
go generate ./internal/...

View File

@ -1,529 +0,0 @@
# go-json
![Go](https://github.com/goccy/go-json/workflows/Go/badge.svg)
[![GoDoc](https://godoc.org/github.com/goccy/go-json?status.svg)](https://pkg.go.dev/github.com/goccy/go-json?tab=doc)
[![codecov](https://codecov.io/gh/goccy/go-json/branch/master/graph/badge.svg)](https://codecov.io/gh/goccy/go-json)
Fast JSON encoder/decoder compatible with encoding/json for Go
<img width="400px" src="https://user-images.githubusercontent.com/209884/92572337-42b42900-f2bf-11ea-973a-c74a359553a5.png"></img>
# Roadmap
```
* version ( expected release date )
* v0.9.0
|
| while maintaining compatibility with encoding/json, we will add convenient APIs
|
v
* v1.0.0
```
We are accepting requests for features that will be implemented between v0.9.0 and v.1.0.0.
If you have the API you need, please submit your issue [here](https://github.com/goccy/go-json/issues).
# Features
- Drop-in replacement of `encoding/json`
- Fast ( See [Benchmark section](https://github.com/goccy/go-json#benchmarks) )
- Flexible customization with options
- Coloring the encoded string
- Can propagate context.Context to `MarshalJSON` or `UnmarshalJSON`
- Can dynamically filter the fields of the structure type-safely
# Installation
```
go get github.com/goccy/go-json
```
# How to use
Replace import statement from `encoding/json` to `github.com/goccy/go-json`
```
-import "encoding/json"
+import "github.com/goccy/go-json"
```
# JSON library comparison
| name | encoder | decoder | compatible with `encoding/json` |
| :----: | :------: | :-----: | :-----------------------------: |
| encoding/json | yes | yes | N/A |
| [json-iterator/go](https://github.com/json-iterator/go) | yes | yes | partial |
| [easyjson](https://github.com/mailru/easyjson) | yes | yes | no |
| [gojay](https://github.com/francoispqt/gojay) | yes | yes | no |
| [segmentio/encoding/json](https://github.com/segmentio/encoding/tree/master/json) | yes | yes | partial |
| [jettison](https://github.com/wI2L/jettison) | yes | no | no |
| [simdjson-go](https://github.com/minio/simdjson-go) | no | yes | no |
| goccy/go-json | yes | yes | yes |
- `json-iterator/go` isn't compatible with `encoding/json` in many ways (e.g. https://github.com/json-iterator/go/issues/229 ), but it hasn't been supported for a long time.
- `segmentio/encoding/json` is well supported for encoders, but some are not supported for decoder APIs such as `Token` ( streaming decode )
## Other libraries
- [jingo](https://github.com/bet365/jingo)
I tried the benchmark but it didn't work.
Also, it seems to panic when it receives an unexpected value because there is no error handling...
- [ffjson](https://github.com/pquerna/ffjson)
Benchmarking gave very slow results.
It seems that it is assumed that the user will use the buffer pool properly.
Also, development seems to have already stopped
# Benchmarks
```
$ cd benchmarks
$ go test -bench .
```
## Encode
<img width="700px" src="https://user-images.githubusercontent.com/209884/107126758-0845cb00-68f5-11eb-8db7-086fcf9bcfaa.png"></img>
<img width="700px" src="https://user-images.githubusercontent.com/209884/107126757-07ad3480-68f5-11eb-87aa-858cc5eacfcb.png"></img>
## Decode
<img width="700" alt="" src="https://user-images.githubusercontent.com/209884/107979944-bd1d6d80-7002-11eb-944b-9d17b6674e3f.png">
<img width="700" alt="" src="https://user-images.githubusercontent.com/209884/107979931-b989e680-7002-11eb-87a0-66fc22d90dd4.png">
<img width="700" alt="" src="https://user-images.githubusercontent.com/209884/107979940-bc84d700-7002-11eb-9647-869bbc25c9d9.png">
# Fuzzing
[go-json-fuzz](https://github.com/goccy/go-json-fuzz) is the repository for fuzzing tests.
If you run the test in this repository and find a bug, please commit to corpus to go-json-fuzz and report the issue to [go-json](https://github.com/goccy/go-json/issues).
# How it works
`go-json` is very fast in both encoding and decoding compared to other libraries.
It's easier to implement by using automatic code generation for performance or by using a dedicated interface, but `go-json` dares to stick to compatibility with `encoding/json` and is the simple interface. Despite this, we are developing with the aim of being the fastest library.
Here, we explain the various speed-up techniques implemented by `go-json`.
## Basic technique
The techniques listed here are the ones used by most of the libraries listed above.
### Buffer reuse
Since the only value required for the result of `json.Marshal(interface{}) ([]byte, error)` is `[]byte`, the only value that must be allocated during encoding is the return value `[]byte` .
Also, as the number of allocations increases, the performance will be affected, so the number of allocations should be kept as low as possible when creating `[]byte`.
Therefore, there is a technique to reduce the number of times a new buffer must be allocated by reusing the buffer used for the previous encoding by using `sync.Pool`.
Finally, you allocate a buffer that is as long as the resulting buffer and copy the contents into it, you only need to allocate the buffer once in theory.
```go
type buffer struct {
data []byte
}
var bufPool = sync.Pool{
New: func() interface{} {
return &buffer{data: make([]byte, 0, 1024)}
},
}
buf := bufPool.Get().(*buffer)
data := encode(buf.data) // reuse buf.data
newBuf := make([]byte, len(data))
copy(newBuf, buf)
buf.data = data
bufPool.Put(buf)
```
### Elimination of reflection
As you know, the reflection operation is very slow.
Therefore, using the fact that the address position where the type information is stored is fixed for each binary ( we call this `typeptr` ),
we can use the address in the type information to call a pre-built optimized process.
For example, you can get the address to the type information from `interface{}` as follows and you can use that information to call a process that does not have reflection.
To process without reflection, pass a pointer (`unsafe.Pointer`) to the value is stored.
```go
type emptyInterface struct {
typ unsafe.Pointer
ptr unsafe.Pointer
}
var typeToEncoder = map[uintptr]func(unsafe.Pointer)([]byte, error){}
func Marshal(v interface{}) ([]byte, error) {
iface := (*emptyInterface)(unsafe.Pointer(&v)
typeptr := uintptr(iface.typ)
if enc, exists := typeToEncoder[typeptr]; exists {
return enc(iface.ptr)
}
...
}
```
※ In reality, `typeToEncoder` can be referenced by multiple goroutines, so exclusive control is required.
## Unique speed-up technique
## Encoder
### Do not escape arguments of `Marshal`
`json.Marshal` and `json.Unmarshal` receive `interface{}` value and they perform type determination dynamically to process.
In normal case, you need to use the `reflect` library to determine the type dynamically, but since `reflect.Type` is defined as `interface`, when you call the method of `reflect.Type`, The reflect's argument is escaped.
Therefore, the arguments for `Marshal` and `Unmarshal` are always escaped to the heap.
However, `go-json` can use the feature of `reflect.Type` while avoiding escaping.
`reflect.Type` is defined as `interface`, but in reality `reflect.Type` is implemented only by the structure `rtype` defined in the `reflect` package.
For this reason, to date `reflect.Type` is the same as `*reflect.rtype`.
Therefore, by directly handling `*reflect.rtype`, which is an implementation of `reflect.Type`, it is possible to avoid escaping because it changes from `interface` to using `struct`.
The technique for working with `*reflect.rtype` directly from `go-json` is implemented at [rtype.go](https://github.com/goccy/go-json/blob/master/internal/runtime/rtype.go)
Also, the same technique is cut out as a library ( https://github.com/goccy/go-reflect )
Initially this feature was the default behavior of `go-json`.
But after careful testing, I found that I passed a large value to `json.Marshal()` and if the argument could not be assigned to the stack, it could not be properly escaped to the heap (a bug in the Go compiler).
Therefore, this feature will be provided as an **optional** until this issue is resolved.
To use it, add `NoEscape` like `MarshalNoEscape()`
### Encoding using opcode sequence
I explained that you can use `typeptr` to call a pre-built process from type information.
In other libraries, this dedicated process is processed by making it an function calling like anonymous function, but function calls are inherently slow processes and should be avoided as much as possible.
Therefore, `go-json` adopted the Instruction-based execution processing system, which is also used to implement virtual machines for programming language.
If it is the first type to encode, create the opcode ( instruction ) sequence required for encoding.
From the second time onward, use `typeptr` to get the cached pre-built opcode sequence and encode it based on it. An example of the opcode sequence is shown below.
```go
json.Marshal(struct{
X int `json:"x"`
Y string `json:"y"`
}{X: 1, Y: "hello"})
```
When encoding a structure like the one above, create a sequence of opcodes like this:
```
- opStructFieldHead ( `{` )
- opStructFieldInt ( `"x": 1,` )
- opStructFieldString ( `"y": "hello"` )
- opStructEnd ( `}` )
- opEnd
```
※ When processing each operation, write the letters on the right.
In addition, each opcode is managed by the following structure (
Pseudo code ).
```go
type opType int
const (
opStructFieldHead opType = iota
opStructFieldInt
opStructFieldStirng
opStructEnd
opEnd
)
type opcode struct {
op opType
key []byte
next *opcode
}
```
The process of encoding using the opcode sequence is roughly implemented as follows.
```go
func encode(code *opcode, b []byte, p unsafe.Pointer) ([]byte, error) {
for {
switch code.op {
case opStructFieldHead:
b = append(b, '{')
code = code.next
case opStructFieldInt:
b = append(b, code.key...)
b = appendInt((*int)(unsafe.Pointer(uintptr(p)+code.offset)))
code = code.next
case opStructFieldString:
b = append(b, code.key...)
b = appendString((*string)(unsafe.Pointer(uintptr(p)+code.offset)))
code = code.next
case opStructEnd:
b = append(b, '}')
code = code.next
case opEnd:
goto END
}
}
END:
return b, nil
}
```
In this way, the huge `switch-case` is used to encode by manipulating the linked list opcodes to avoid unnecessary function calls.
### Opcode sequence optimization
One of the advantages of encoding using the opcode sequence is the ease of optimization.
The opcode sequence mentioned above is actually converted into the following optimized operations and used.
```
- opStructFieldHeadInt ( `{"x": 1,` )
- opStructEndString ( `"y": "hello"}` )
- opEnd
```
It has been reduced from 5 opcodes to 3 opcodes !
Reducing the number of opcodees means reducing the number of branches with `switch-case`.
In other words, the closer the number of operations is to 1, the faster the processing can be performed.
In `go-json`, optimization to reduce the number of opcodes itself like the above and it speeds up by preparing opcodes with optimized paths.
### Change recursive call from CALL to JMP
Recursive processing is required during encoding if the type is defined recursively as follows:
```go
type T struct {
X int
U *U
}
type U struct {
T *T
}
b, err := json.Marshal(&T{
X: 1,
U: &U{
T: &T{
X: 2,
},
},
})
fmt.Println(string(b)) // {"X":1,"U":{"T":{"X":2,"U":null}}}
```
In `go-json`, recursive processing is processed by the operation type of ` opStructFieldRecursive`.
In this operation, after acquiring the opcode sequence used for recursive processing, the function is **not** called recursively as it is, but the necessary values are saved by itself and implemented by moving to the next operation.
The technique of implementing recursive processing with the `JMP` operation while avoiding the `CALL` operation is a famous technique for implementing a high-speed virtual machine.
For more details, please refer to [the article](https://engineering.mercari.com/blog/entry/1599563768-081104c850) ( but Japanese only ).
### Dispatch by typeptr from map to slice
When retrieving the data cached from the type information by `typeptr`, we usually use map.
Map requires exclusive control, so use `sync.Map` for a naive implementation.
However, this is slow, so it's a good idea to use the `atomic` package for exclusive control as implemented by `segmentio/encoding/json` ( https://github.com/segmentio/encoding/blob/master/json/codec.go#L41-L55 ).
This implementation slows down the set instead of speeding up the get, but it works well because of the nature of the library, it encodes much more for the same type.
However, as a result of profiling, I noticed that `runtime.mapaccess2` accounts for a significant percentage of the execution time. So I thought if I could change the lookup from map to slice.
There is an API named `typelinks` defined in the `runtime` package that the `reflect` package uses internally.
This allows you to get all the type information defined in the binary at runtime.
The fact that all type information can be acquired means that by constructing slices in advance with the acquired total number of type information, it is possible to look up with the value of `typeptr` without worrying about out-of-range access.
However, if there is too much type information, it will use a lot of memory, so by default we will only use this optimization if the slice size fits within **2Mib** .
If this approach is not available, it will fall back to the `atomic` based process described above.
If you want to know more, please refer to the implementation [here](https://github.com/goccy/go-json/blob/master/internal/runtime/type.go#L36-L100)
## Decoder
### Dispatch by typeptr from map to slice
Like the encoder, the decoder also uses typeptr to call the dedicated process.
### Faster termination character inspection using NUL character
In order to decode, you have to traverse the input buffer character by position.
At that time, if you check whether the buffer has reached the end, it will be very slow.
`buf` : `[]byte` type variable. holds the string passed to the decoder
`cursor` : `int64` type variable. holds the current read position
```go
buflen := len(buf)
for ; cursor < buflen; cursor++ { // compare cursor and buflen at all times, it is so slow.
switch buf[cursor] {
case ' ', '\n', '\r', '\t':
}
}
```
Therefore, by adding the `NUL` (`\000`) character to the end of the read buffer as shown below, it is possible to check the termination character at the same time as other characters.
```go
for {
switch buf[cursor] {
case ' ', '\n', '\r', '\t':
case '\000':
return nil
}
cursor++
}
```
### Use Boundary Check Elimination
Due to the `NUL` character optimization, the Go compiler does a boundary check every time, even though `buf[cursor]` does not cause out-of-range access.
Therefore, `go-json` eliminates boundary check by fetching characters for hotspot by pointer operation. For example, the following code.
```go
func char(ptr unsafe.Pointer, offset int64) byte {
return *(*byte)(unsafe.Pointer(uintptr(ptr) + uintptr(offset)))
}
p := (*sliceHeader)(&unsafe.Pointer(buf)).data
for {
switch char(p, cursor) {
case ' ', '\n', '\r', '\t':
case '\000':
return nil
}
cursor++
}
```
### Checking the existence of fields of struct using Bitmaps
I found by the profiling result, in the struct decode, lookup process for field was taking a long time.
For example, consider decoding a string like `{"a":1,"b":2,"c":3}` into the following structure:
```go
type T struct {
A int `json:"a"`
B int `json:"b"`
C int `json:"c"`
}
```
At this time, it was found that it takes a lot of time to acquire the decoding process corresponding to the field from the field name as shown below during the decoding process.
```go
fieldName := decodeKey(buf, cursor) // "a" or "b" or "c"
decoder, exists := fieldToDecoderMap[fieldName] // so slow
if exists {
decoder(buf, cursor)
} else {
skipValue(buf, cursor)
}
```
To improve this process, `json-iterator/go` is optimized so that it can be branched by switch-case when the number of fields in the structure is 10 or less (switch-case is faster than map). However, there is a risk of hash collision because the value hashed by the FNV algorithm is used for conditional branching. Also, `gojay` processes this part at high speed by letting the library user yourself write `switch-case`.
`go-json` considers and implements a new approach that is different from these. I call this **bitmap field optimization**.
The range of values per character can be represented by `[256]byte`. Also, if the number of fields in the structure is 8 or less, `int8` type can represent the state of each field.
In other words, it has the following structure.
- Base ( 8bit ): `00000000`
- Key "a": `00000001` ( assign key "a" to the first bit )
- Key "b": `00000010` ( assign key "b" to the second bit )
- Key "c": `00000100` ( assign key "c" to the third bit )
Bitmap structure is the following
```
| key index(0) |
------------------------
0 | 00000000 |
1 | 00000000 |
~~ | |
97 (a) | 00000001 |
98 (b) | 00000010 |
99 (c) | 00000100 |
~~ | |
255 | 00000000 |
```
You can think of this as a Bitmap with a height of `256` and a width of the maximum string length in the field name.
In other words, it can be represented by the following type .
```go
[maxFieldKeyLength][256]int8
```
When decoding a field character, check whether the corresponding character exists by referring to the pre-built bitmap like the following.
```go
var curBit int8 = math.MaxInt8 // 11111111
c := char(buf, cursor)
bit := bitmap[keyIdx][c]
curBit &= bit
if curBit == 0 {
// not found field
}
```
If `curBit` is not `0` until the end of the field string, then the string is
You may have hit one of the fields.
But the possibility is that if the decoded string is shorter than the field string, you will get a false hit.
- input: `{"a":1}`
```go
type T struct {
X int `json:"abc"`
}
```
※ Since `a` is shorter than `abc`, it can decode to the end of the field character without `curBit` being 0.
Rest assured. In this case, it doesn't matter because you can tell if you hit by comparing the string length of `a` with the string length of `abc`.
Finally, calculate the position of the bit where `1` is set and get the corresponding value, and you're done.
Using this technique, field lookups are possible with only bitwise operations and access to slices.
`go-json` uses a similar technique for fields with 9 or more and 16 or less fields. At this time, Bitmap is constructed as `[maxKeyLen][256]int16` type.
Currently, this optimization is not performed when the maximum length of the field name is long (specifically, 64 bytes or more) in addition to the limitation of the number of fields from the viewpoint of saving memory usage.
### Others
I have done a lot of other optimizations. I will find time to write about them. If you have any questions about what's written here or other optimizations, please visit the `#go-json` channel on `gophers.slack.com` .
## Reference
Regarding the story of go-json, there are the following articles in Japanese only.
- https://speakerdeck.com/goccy/zui-su-falsejsonraiburariwoqiu-mete
- https://engineering.mercari.com/blog/entry/1599563768-081104c850/
# Looking for Sponsors
I'm looking for sponsors this library. This library is being developed as a personal project in my spare time. If you want a quick response or problem resolution when using this library in your project, please register as a [sponsor](https://github.com/sponsors/goccy). I will cooperate as much as possible. Of course, this library is developed as an MIT license, so you can use it freely for free.
# License
MIT

View File

@ -1,68 +0,0 @@
package json
import (
"fmt"
"github.com/goccy/go-json/internal/encoder"
)
type (
ColorFormat = encoder.ColorFormat
ColorScheme = encoder.ColorScheme
)
const escape = "\x1b"
type colorAttr int
//nolint:deadcode,varcheck
const (
fgBlackColor colorAttr = iota + 30
fgRedColor
fgGreenColor
fgYellowColor
fgBlueColor
fgMagentaColor
fgCyanColor
fgWhiteColor
)
//nolint:deadcode,varcheck
const (
fgHiBlackColor colorAttr = iota + 90
fgHiRedColor
fgHiGreenColor
fgHiYellowColor
fgHiBlueColor
fgHiMagentaColor
fgHiCyanColor
fgHiWhiteColor
)
func createColorFormat(attr colorAttr) ColorFormat {
return ColorFormat{
Header: wrapColor(attr),
Footer: resetColor(),
}
}
func wrapColor(attr colorAttr) string {
return fmt.Sprintf("%s[%dm", escape, attr)
}
func resetColor() string {
return wrapColor(colorAttr(0))
}
var (
DefaultColorScheme = &ColorScheme{
Int: createColorFormat(fgHiMagentaColor),
Uint: createColorFormat(fgHiMagentaColor),
Float: createColorFormat(fgHiMagentaColor),
Bool: createColorFormat(fgHiYellowColor),
String: createColorFormat(fgHiGreenColor),
Binary: createColorFormat(fgHiRedColor),
ObjectKey: createColorFormat(fgHiCyanColor),
Null: createColorFormat(fgBlueColor),
}
)

Some files were not shown because too many files have changed in this diff Show More