Compare commits

..

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

1210 changed files with 54912 additions and 149954 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

View file

@ -1,16 +0,0 @@
---
name: "builder"
description: "builder"
inputs:
steps:
description: "commands"
require: true
default: ""
outputs: {}
runs:
using: "docker"
image: "docker://golang:1.25"
args:
- ${{ inputs.steps }}
entrypoint: /bin/sh -c

View file

@ -1,39 +0,0 @@
---
name: build & test
'on':
push:
branches:
- '*'
jobs:
build:
runs-on:
- linux
- amd64
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
container:
image: ghcr.io/catthehacker/ubuntu:act-24.04
steps:
- name: Checkout
uses: actions/checkout@v6
with:
lfs: true
- name: Cache
id: pki-cache
uses: actions/cache@v4
with:
path: go-build
key: pki-${{ hashFiles('**/go.mod','**/go.sum') }}
- name: Build
uses: ./.github/actions/builder
with:
steps: |
go generate cmd/pki/*.go
go build -mod=vendor -o pki -v cmd/pki/*.go
env:
GOOS: linux
GOARCH: amd64
GOCACHE: ${{ forgejo.workspace }}/go-build
VERSION: ${{ forgejo.ref_name }}

View file

@ -1,90 +0,0 @@
---
name: build & release
'on':
push:
tags:
- '*'
pull_request:
branches:
- master
jobs:
build:
runs-on:
- linux
- ${{ matrix.arch }}
strategy:
matrix:
os:
- linux
arch:
- amd64
- arm64
- arm
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
container:
image: ghcr.io/catthehacker/ubuntu:act-24.04
steps:
- name: Checkout
uses: actions/checkout@v6
with:
lfs: true
- name: Cache
id: pki-cache
uses: actions/cache@v4
with:
path: go-build
key: pki-${{ matrix.arch }}-${{ hashFiles('**/go.mod','**/go.sum') }}
- name: Build pki
uses: ./.github/actions/builder
with:
steps: |
mkdir $GOARCH artifacts
go generate cmd/pki/*.go
go build -mod=vendor -o $GOARCH/pki -v cmd/pki/*.go
env:
GOOS: linux
GOARCH: ${{ matrix.arch }}
GOCACHE: ${{ forgejo.workspace }}/go-build
VERSION: ${{ forgejo.ref_name }}
- name: Create release archive
if: startsWith(forgejo.ref, 'refs/tags/')
run: |
cd ${{ matrix.arch }}
tar -czvf ../artifacts/pki-${{ github.ref_name }}-${{ matrix.os }}-${{ matrix.arch }}.tar.gz pki
- name: Upload artifacts
uses: actions/upload-artifact@v3
if: startsWith(forgejo.ref, 'refs/tags/')
with:
name: pki-artifacts
path: "artifacts/*"
release:
runs-on:
- linux
needs: [build]
container:
image: ghcr.io/catthehacker/ubuntu:act-24.04
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
if: startsWith(forgejo.ref, 'refs/tags/')
steps:
- name: Download artifacts
uses: actions/download-artifact@v3
with:
name: pki-artifacts
path: artifacts
- name: Create checksum files
run: |
cd artifacts
sha256sum *.tar.gz > sha256sum.txt
sha512sum *.tar.gz > sha512sum.txt
- name: Create Release
if: startsWith(forgejo.ref, 'refs/tags/')
id: create_release
uses: actions/forgejo-release@v2.7.3
with:
direction: upload
release-dir: artifacts/
title: "pki ${{ forgejo.ref_name }}"

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

@ -1,6 +1,5 @@
# pki # pki
[![Build Status](https://drone.paulbsd.com/api/badges/paulbsd/pki/status.svg)](https://drone.paulbsd.com/paulbsd/pki)
[![Build Status](https://git.paulbsd.com/paulbsd/pki/actions/workflows/build.yml/badge.svg)](https://git.paulbsd.com/paulbsd/pki/actions)
## Summary ## Summary
@ -11,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
@ -41,7 +40,7 @@ ovhck=
## License ## License
```text ```text
Copyright (c) 2020, 2021, 2022 PaulBSD Copyright (c) 2020, 2021 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

62
ci-build.sh Executable file
View file

@ -0,0 +1,62 @@
#!/bin/bash
set -e
PROJECTNAME=pki
RELEASENAME=${PROJECTNAME}
VERSION="0"
GOOPTIONS="-mod=vendor"
SRCFILES=cmd/${PROJECTNAME}/*.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

56
go.mod
View file

@ -1,43 +1,41 @@
module git.paulbsd.com/paulbsd/pki module git.paulbsd.com/paulbsd/pki
go 1.25 go 1.17
require ( require (
github.com/go-acme/lego/v4 v4.29.0 github.com/go-acme/lego/v4 v4.4.0
github.com/golang/snappy v1.0.0 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/labstack/echo/v4 v4.14.0 github.com/google/go-cmp v0.5.5 // indirect
github.com/lib/pq v1.10.9 github.com/gopherjs/gopherjs v0.0.0-20210406100015-1e088ea4ee04 // indirect
github.com/miekg/dns v1.1.69 // indirect github.com/labstack/echo/v4 v4.5.0
github.com/lib/pq v1.10.3
github.com/miekg/dns v1.1.43 // indirect
github.com/onsi/ginkgo v1.16.0 // indirect github.com/onsi/ginkgo v1.16.0 // indirect
github.com/onsi/gomega v1.11.0 // indirect github.com/onsi/gomega v1.11.0 // indirect
golang.org/x/crypto v0.46.0 // indirect github.com/smartystreets/assertions v1.2.0 // indirect
golang.org/x/net v0.48.0 // indirect golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
golang.org/x/sys v0.39.0 // indirect golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f // indirect
golang.org/x/text v0.32.0 // indirect golang.org/x/sys v0.0.0-20210903071746-97244b99971b // indirect
golang.org/x/time v0.14.0 // indirect golang.org/x/text v0.3.7 // indirect
gopkg.in/ini.v1 v1.67.0 golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
xorm.io/builder v0.3.13 // indirect gopkg.in/ini.v1 v1.62.1
xorm.io/xorm v1.3.11 xorm.io/builder v0.3.9 // indirect
xorm.io/xorm v1.2.3
) )
require ( require (
github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v4 v4.1.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/goccy/go-json v0.7.8 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect github.com/json-iterator/go v1.1.11 // indirect
github.com/labstack/gommon v0.4.2 // indirect github.com/labstack/gommon v0.3.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.13 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/ovh/go-ovh v1.9.0 // indirect github.com/ovh/go-ovh v1.1.0 // indirect
github.com/syndtr/goleveldb v1.0.0 // indirect github.com/syndtr/goleveldb v1.0.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect github.com/valyala/fasttemplate v1.2.1 // indirect
golang.org/x/mod v0.31.0 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/tools v0.40.0 // indirect
) )

1096
go.sum

File diff suppressed because it is too large Load diff

View file

@ -2,13 +2,10 @@ 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"` Domains 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 `xorm:"notnull"`

View file

@ -2,6 +2,7 @@ package config
import ( import (
"flag" "flag"
"fmt"
"git.paulbsd.com/paulbsd/pki/utils" "git.paulbsd.com/paulbsd/pki/utils"
"github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/lego"
@ -38,6 +39,7 @@ func (cfg *Config) GetConfig() error {
} }
pkisection := inicfg.Section("pki") pkisection := inicfg.Section("pki")
options := make(map[string]string)
cfg.DbParams.DbHostname = pkisection.Key("db_hostname").MustString("localhost") cfg.DbParams.DbHostname = pkisection.Key("db_hostname").MustString("localhost")
cfg.DbParams.DbName = pkisection.Key("db_name").MustString("database") cfg.DbParams.DbName = pkisection.Key("db_name").MustString("database")
@ -53,13 +55,23 @@ func (cfg *Config) GetConfig() error {
cfg.ACME.Env = pkisection.Key("env").MustString("prod") cfg.ACME.Env = pkisection.Key("env").MustString("prod")
cfg.ACME.MaxDaysBefore = pkisection.Key("maxdaysbefore").MustInt(0) cfg.ACME.MaxDaysBefore = pkisection.Key("maxdaysbefore").MustInt(0)
options["ovhendpoint"] = pkisection.Key("ovhendpoint").MustString("ovh-eu")
options["ovhak"] = pkisection.Key("ovhak").MustString("")
options["ovhas"] = pkisection.Key("ovhas").MustString("")
options["ovhck"] = pkisection.Key("ovhck").MustString("")
cfg.ACME.ProviderOptions = options
for k, v := range options {
if v == "" {
utils.Advice(fmt.Sprintf("OVH provider parameter %s not set", k))
}
}
switch cfg.ACME.Env { switch cfg.ACME.Env {
case "prod": case "prod":
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
@ -88,6 +100,7 @@ type Config struct {
ACME struct { ACME struct {
Env string `json:"env"` Env string `json:"env"`
AuthURL string `json:"authurl"` AuthURL string `json:"authurl"`
ProviderOptions map[string]string `json:"provideroptions"`
MaxDaysBefore int `json:"maxdaysbefore"` MaxDaysBefore int `json:"maxdaysbefore"`
} }
Init struct { Init struct {

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"
@ -17,8 +16,8 @@ import (
// Init creates connection to database and exec Schema // Init creates connection to database and exec Schema
func Init(cfg *config.Config) (err error) { func Init(cfg *config.Config) (err error) {
var databaseEngine = "postgres" var databaseEngine = "postgres"
tables := []any{cert.Entry{}, tables := []interface{}{cert.Entry{},
pki.User{}, domain.Domain{}, pki.Provider{}} 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",
@ -42,14 +41,8 @@ func Init(cfg *config.Config) (err error) {
log.Println("Syncing tables") log.Println("Syncing tables")
for _, table := range tables { for _, table := range tables {
err = cfg.Db.CreateTables(table) cfg.Db.CreateTables(table)
if err != nil { cfg.Db.Sync2(table)
log.Fatalln(err)
}
err = cfg.Db.Sync(table)
if err != nil {
log.Fatalln(err)
}
} }
return return
} }

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

@ -9,13 +9,12 @@ import (
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"log" "log"
"strings"
"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,12 +29,15 @@ 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, domains []string) (Entry cert.Entry, err error) {
has, err := cfg.Db.Where("domain = ?", domain).And(
has, err := cfg.Db.Where("domains = ?", strings.Join(domains, ",")).And(
"auth_url = ?", cfg.ACME.AuthURL).And( "auth_url = ?", cfg.ACME.AuthURL).And(
fmt.Sprintf("validity_end::timestamp-'%d DAY'::INTERVAL >= now()", cfg.ACME.MaxDaysBefore)).Desc( fmt.Sprintf("validity_end::timestamp-'%d DAY'::INTERVAL >= now()", cfg.ACME.MaxDaysBefore)).Desc(
"id").Get(&Entry) "id").Get(&Entry)
fmt.Println(has, err)
if !has { if !has {
err = fmt.Errorf("entry doesn't exists") err = fmt.Errorf("entry doesn't exists")
} }
@ -66,45 +68,12 @@ 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, domains []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
var pkiprovider Provider
_, err = cfg.Db.Where("name = ?", dom.Provider).Get(&pkiprovider)
if err != nil {
log.Println(err)
return
}
switch dom.Provider {
case "ovh":
provider, err = pkiprovider.initOVHProvider()
case "pdns":
provider, err = pkiprovider.initPowerDNSProvider()
default:
return
}
if err != nil { if err != nil {
log.Println(err) log.Println(err)
} }
@ -114,7 +83,7 @@ func (u *User) RequestNewCert(cfg *config.Config, domainnames *[]string) (certs
log.Println(err) log.Println(err)
} }
err = client.Challenge.SetDNS01Provider(provider) err = client.Challenge.SetDNS01Provider(ovhprovider)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
} }
@ -128,15 +97,14 @@ func (u *User) RequestNewCert(cfg *config.Config, domainnames *[]string) (certs
} }
request := certificate.ObtainRequest{ request := certificate.ObtainRequest{
Domains: *domainnames, Domains: domains,
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.Println(err)
} }
return return
} }

View file

@ -1,65 +1,20 @@
package pki package pki
import ( import (
"encoding/json" "git.paulbsd.com/paulbsd/pki/src/config"
"log"
"net/url"
"time"
"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 (p *Provider) initOVHProvider() (ovhprovider *ovh.DNSProvider, err error) { func initProvider(cfg *config.Config) (ovhprovider *ovh.DNSProvider, err error) {
ovhconfig := ovh.NewDefaultConfig() ovhconfig := ovh.NewDefaultConfig()
var data = make(map[string]string) ovhconfig.APIEndpoint = cfg.ACME.ProviderOptions["ovhendpoint"]
err = json.Unmarshal([]byte(p.Config), &data) ovhconfig.ApplicationKey = cfg.ACME.ProviderOptions["ovhak"]
if err != nil { ovhconfig.ApplicationSecret = cfg.ACME.ProviderOptions["ovhas"]
log.Println(err) ovhconfig.ConsumerKey = cfg.ACME.ProviderOptions["ovhck"]
return
}
ovhconfig.APIEndpoint = data["ovhendpoint"]
ovhconfig.ApplicationKey = data["ovhak"]
ovhconfig.ApplicationSecret = data["ovhas"]
ovhconfig.ConsumerKey = data["ovhck"]
ovhprovider, err = ovh.NewDNSProviderConfig(ovhconfig) ovhprovider, err = ovh.NewDNSProviderConfig(ovhconfig)
if err != nil {
log.Println(err)
}
return return
} }
// initPowerDNSProvider initialize DNS provider configuration
func (p *Provider) initPowerDNSProvider() (pdnsprovider *pdns.DNSProvider, err error) {
pdnsconfig := pdns.NewDefaultConfig()
var data = make(map[string]string)
err = json.Unmarshal([]byte(p.Config), &data)
if err != nil {
log.Println(err)
return
}
pdnsconfig.Host, err = url.Parse(data["pdnsapiurl"])
pdnsconfig.APIKey = data["pdnsapikey"]
pdnsprovider, err = pdns.NewDNSProviderConfig(pdnsconfig)
if err != nil {
log.Println(err)
}
return
}
type Provider struct {
ID int `xorm:"pk autoincr"`
Name string `xorm:"text notnull unique"`
Config string `xorm:"json notnull"`
Created time.Time `xorm:"created notnull"`
Updated time.Time `xorm:"updated notnull"`
}

View file

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"strings"
"git.paulbsd.com/paulbsd/pki/src/config" "git.paulbsd.com/paulbsd/pki/src/config"
"git.paulbsd.com/paulbsd/pki/src/pki" "git.paulbsd.com/paulbsd/pki/src/pki"
@ -29,18 +30,13 @@ func RunServer(cfg *config.Config) (err error) {
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)") return c.String(http.StatusOK, "Welcome to PKI software (https://git.paulbsd.com/paulbsd/pki)")
}) })
e.POST("/cert", func(c echo.Context) (err error) { e.GET("/domain/:domains", func(c echo.Context) (err error) {
var request = new(EntryRequest) var result EntryResponse
var result = make(map[string]EntryResponse) var domains = strings.Split(c.Param("domains"), ",")
err = c.Bind(&request)
if err != nil {
log.Println(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()) log.Println(fmt.Sprintf("Providing %s to user %s at %s", domains, c.Get("username"), c.RealIP()))
result, err = GetCertificate(cfg, c.Get("user").(*pki.User), &request.Domains) result, err = GetCertificate(cfg, c.Get("user").(*pki.User), domains)
if err != nil { if err != nil {
return c.String(http.StatusInternalServerError, fmt.Sprintf("%s", err)) return c.String(http.StatusInternalServerError, fmt.Sprintf("%s", err))
} }

View file

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"log" "log"
"regexp" "regexp"
"strings"
"time" "time"
"git.paulbsd.com/paulbsd/pki/src/cert" "git.paulbsd.com/paulbsd/pki/src/cert"
@ -13,24 +14,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, domains []string) (result EntryResponse, err error) {
err = CheckDomains(domains) err = CheckDomains(domains)
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, domains)
entry, err := user.GetEntry(cfg, &firstdomain)
if err != nil { if err != nil {
certs, err := user.RequestNewCert(cfg, domains) certs, err := user.RequestNewCert(cfg, domains)
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 +33,33 @@ 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{Domains: strings.Join(domains, ","),
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 // CheckDomains check if requested domains are valid
func CheckDomains(domains *[]string) (err error) { func CheckDomains(domains []string) (err error) {
for _, domain := range *domains { domainRegex, err := regexp.Compile(`^[a-z0-9\*]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,6}$`)
err = CheckDomain(&domain)
if err != nil { if err != nil {
return return
} }
}
return
}
// CheckDomain check if requested domain are valid for _, d := range domains {
func CheckDomain(domain *string) (err error) { res := domainRegex.Match([]byte(d))
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(fmt.Sprintf("Domain %s has not a valid syntax %s, please verify", d, err))
}
} }
return return
} }
@ -88,7 +80,9 @@ func GetDates(cert []byte) (NotBefore time.Time, NotAfter time.Time, err error)
// 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) timeformatstring := "2006-01-02 15:04:05"
out.Domains = in.Domains
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.Format(timeformatstring)
@ -97,14 +91,9 @@ func convertEntryToResponse(in cert.Entry) (out EntryResponse) {
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"` Domains string `json:"domains"`
Certificate string `json:"certificate"` Certificate string `json:"certificate"`
PrivateKey string `json:"privatekey"` PrivateKey string `json:"privatekey"`
ValidityBegin string `json:"validitybegin"` ValidityBegin string `json:"validitybegin"`

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

@ -0,0 +1,10 @@
language: go
go:
- 1.13
- 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] # 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].
@ -9,11 +9,9 @@ The retries exponentially increase and stop increasing when a certain threshold
## Usage ## Usage
Import path is `github.com/cenkalti/backoff/v5`. Please note the version part at the end. Import path is `github.com/cenkalti/backoff/v4`. Please note the version part at the end.
For most cases, use `Retry` function. See [example_test.go][example] for an example. Use https://pkg.go.dev/github.com/cenkalti/backoff/v4 to view the documentation.
If you have specific needs, copy `Retry` function (from [retry.go][retry-src]) into your code and modify it as needed.
## Contributing ## Contributing
@ -21,11 +19,14 @@ If you have specific needs, copy `Retry` function (from [retry.go][retry-src]) i
* 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/v5 [godoc]: https://pkg.go.dev/github.com/cenkalti/backoff/v4
[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 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
[retry-src]: https://github.com/cenkalti/backoff/blob/v5/retry.go [advanced example]: https://pkg.go.dev/github.com/cenkalti/backoff/v4?tab=doc#pkg-examples
[example]: https://github.com/cenkalti/backoff/blob/v5/example_test.go

View file

@ -15,12 +15,12 @@ import "time"
// BackOff is a backoff policy for retrying an operation. // BackOff is a backoff policy for retrying an operation.
type BackOff interface { type BackOff interface {
// NextBackOff returns the duration to wait before retrying the operation, // NextBackOff returns the duration to wait before retrying the operation,
// backoff.Stop to indicate that no more retries should be made. // or backoff. Stop to indicate that no more retries should be made.
// //
// Example usage: // Example usage:
// //
// duration := backoff.NextBackOff() // duration := backoff.NextBackOff();
// if duration == backoff.Stop { // if (duration == backoff.Stop) {
// // Do not retry operation. // // Do not retry operation.
// } else { // } else {
// // Sleep for duration and retry operation. // // Sleep for duration and retry operation.

62
vendor/github.com/cenkalti/backoff/v4/context.go generated vendored Normal file
View file

@ -0,0 +1,62 @@
package backoff
import (
"context"
"time"
)
// BackOffContext is a backoff policy that stops retrying after the context
// is canceled.
type BackOffContext interface { // nolint: golint
BackOff
Context() context.Context
}
type backOffContext struct {
BackOff
ctx context.Context
}
// WithContext returns a BackOffContext with context ctx
//
// ctx must not be nil
func WithContext(b BackOff, ctx context.Context) BackOffContext { // nolint: golint
if ctx == nil {
panic("nil context")
}
if b, ok := b.(*backOffContext); ok {
return &backOffContext{
BackOff: b.BackOff,
ctx: ctx,
}
}
return &backOffContext{
BackOff: b,
ctx: ctx,
}
}
func getContext(b BackOff) context.Context {
if cb, ok := b.(BackOffContext); ok {
return cb.Context()
}
if tb, ok := b.(*backOffTries); ok {
return getContext(tb.delegate)
}
return context.Background()
}
func (b *backOffContext) Context() context.Context {
return b.ctx
}
func (b *backOffContext) NextBackOff() time.Duration {
select {
case <-b.ctx.Done():
return Stop
default:
return b.BackOff.NextBackOff()
}
}

View file

@ -1,7 +1,7 @@
package backoff package backoff
import ( import (
"math/rand/v2" "math/rand"
"time" "time"
) )
@ -28,7 +28,13 @@ multiplied by the exponential, that is, between 2 and 6 seconds.
Note: MaxInterval caps the RetryInterval and not the randomized interval. Note: MaxInterval caps the RetryInterval and not the randomized interval.
Example: Given the following default arguments, for 9 tries the sequence will be: If the time elapsed since an ExponentialBackOff instance is created goes past the
MaxElapsedTime, then the method NextBackOff() starts returning backoff.Stop.
The elapsed time can be reset by calling Reset().
Example: Given the following default arguments, for 10 tries the sequence will be,
and assuming we go over the MaxElapsedTime on the 10th try:
Request # RetryInterval (seconds) Randomized Interval (seconds) Request # RetryInterval (seconds) Randomized Interval (seconds)
@ -41,6 +47,7 @@ Example: Given the following default arguments, for 9 tries the sequence will be
7 5.692 [2.846, 8.538] 7 5.692 [2.846, 8.538]
8 8.538 [4.269, 12.807] 8 8.538 [4.269, 12.807]
9 12.807 [6.403, 19.210] 9 12.807 [6.403, 19.210]
10 19.210 backoff.Stop
Note: Implementation is not thread-safe. Note: Implementation is not thread-safe.
*/ */
@ -49,8 +56,19 @@ type ExponentialBackOff struct {
RandomizationFactor float64 RandomizationFactor float64
Multiplier float64 Multiplier float64
MaxInterval time.Duration MaxInterval time.Duration
// After MaxElapsedTime the ExponentialBackOff returns Stop.
// It never stops if MaxElapsedTime == 0.
MaxElapsedTime time.Duration
Stop time.Duration
Clock Clock
currentInterval time.Duration currentInterval time.Duration
startTime time.Time
}
// Clock is an interface that returns current time for BackOff.
type Clock interface {
Now() time.Time
} }
// Default values for ExponentialBackOff. // Default values for ExponentialBackOff.
@ -59,37 +77,63 @@ const (
DefaultRandomizationFactor = 0.5 DefaultRandomizationFactor = 0.5
DefaultMultiplier = 1.5 DefaultMultiplier = 1.5
DefaultMaxInterval = 60 * time.Second DefaultMaxInterval = 60 * time.Second
DefaultMaxElapsedTime = 15 * time.Minute
) )
// NewExponentialBackOff creates an instance of ExponentialBackOff using default values. // NewExponentialBackOff creates an instance of ExponentialBackOff using default values.
func NewExponentialBackOff() *ExponentialBackOff { func NewExponentialBackOff() *ExponentialBackOff {
return &ExponentialBackOff{ b := &ExponentialBackOff{
InitialInterval: DefaultInitialInterval, InitialInterval: DefaultInitialInterval,
RandomizationFactor: DefaultRandomizationFactor, RandomizationFactor: DefaultRandomizationFactor,
Multiplier: DefaultMultiplier, Multiplier: DefaultMultiplier,
MaxInterval: DefaultMaxInterval, MaxInterval: DefaultMaxInterval,
MaxElapsedTime: DefaultMaxElapsedTime,
Stop: Stop,
Clock: SystemClock,
} }
b.Reset()
return b
} }
type systemClock struct{}
func (t systemClock) Now() time.Time {
return time.Now()
}
// SystemClock implements Clock interface that uses time.Now().
var SystemClock = systemClock{}
// Reset the interval back to the initial retry interval and restarts the timer. // Reset the interval back to the initial retry interval and restarts the timer.
// Reset must be called before using b. // Reset must be called before using b.
func (b *ExponentialBackOff) Reset() { func (b *ExponentialBackOff) Reset() {
b.currentInterval = b.InitialInterval b.currentInterval = b.InitialInterval
b.startTime = b.Clock.Now()
} }
// NextBackOff calculates the next backoff interval using the formula: // NextBackOff calculates the next backoff interval using the formula:
//
// Randomized interval = RetryInterval * (1 ± RandomizationFactor) // Randomized interval = RetryInterval * (1 ± RandomizationFactor)
func (b *ExponentialBackOff) NextBackOff() time.Duration { func (b *ExponentialBackOff) NextBackOff() time.Duration {
if b.currentInterval == 0 { // Make sure we have not gone over the maximum elapsed time.
b.currentInterval = b.InitialInterval elapsed := b.GetElapsedTime()
}
next := getRandomValueFromInterval(b.RandomizationFactor, rand.Float64(), b.currentInterval) next := getRandomValueFromInterval(b.RandomizationFactor, rand.Float64(), b.currentInterval)
b.incrementCurrentInterval() b.incrementCurrentInterval()
if b.MaxElapsedTime != 0 && elapsed+next > b.MaxElapsedTime {
return b.Stop
}
return next return next
} }
// GetElapsedTime returns the elapsed time since an ExponentialBackOff instance
// is created and is reset when Reset() is called.
//
// The elapsed time is computed using time.Now().UnixNano(). It is
// safe to call even while the backoff policy is used by a running
// ticker.
func (b *ExponentialBackOff) GetElapsedTime() time.Duration {
return b.Clock.Now().Sub(b.startTime)
}
// Increments the current interval by multiplying it with the multiplier. // Increments the current interval by multiplying it with the multiplier.
func (b *ExponentialBackOff) incrementCurrentInterval() { func (b *ExponentialBackOff) incrementCurrentInterval() {
// Check for overflow, if overflow is detected set the current interval to the max interval. // Check for overflow, if overflow is detected set the current interval to the max interval.
@ -101,12 +145,8 @@ 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

112
vendor/github.com/cenkalti/backoff/v4/retry.go generated vendored Normal file
View file

@ -0,0 +1,112 @@
package backoff
import (
"errors"
"time"
)
// An Operation is executing by Retry() or RetryNotify().
// The operation will be retried using a backoff policy if it returns an error.
type Operation func() error
// Notify is a notify-on-error function. It receives an operation error and
// backoff delay if the operation failed (with an error).
//
// NOTE that if the backoff policy stated to stop retrying,
// the notify function isn't called.
type Notify func(error, time.Duration)
// Retry the operation o until it does not return error or BackOff stops.
// o is guaranteed to be run at least once.
//
// If o returns a *PermanentError, the operation is not retried, and the
// wrapped error is returned.
//
// Retry sleeps the goroutine for the duration returned by BackOff after a
// failed operation returns.
func Retry(o Operation, b BackOff) error {
return RetryNotify(o, b, nil)
}
// RetryNotify calls notify function with the error and wait duration
// for each failed attempt before sleep.
func RetryNotify(operation Operation, b BackOff, notify Notify) error {
return RetryNotifyWithTimer(operation, b, notify, nil)
}
// RetryNotifyWithTimer calls notify function with the error and wait duration using the given Timer
// for each failed attempt before sleep.
// A default timer that uses system timer is used when nil is passed.
func RetryNotifyWithTimer(operation Operation, b BackOff, notify Notify, t Timer) error {
var err error
var next time.Duration
if t == nil {
t = &defaultTimer{}
}
defer func() {
t.Stop()
}()
ctx := getContext(b)
b.Reset()
for {
if err = operation(); err == nil {
return nil
}
var permanent *PermanentError
if errors.As(err, &permanent) {
return permanent.Err
}
if next = b.NextBackOff(); next == Stop {
if cerr := ctx.Err(); cerr != nil {
return cerr
}
return err
}
if notify != nil {
notify(err, next)
}
t.Start(next)
select {
case <-ctx.Done():
return ctx.Err()
case <-t.C():
}
}
}
// PermanentError signals that the operation should not be retried.
type PermanentError struct {
Err error
}
func (e *PermanentError) Error() string {
return e.Err.Error()
}
func (e *PermanentError) Unwrap() error {
return e.Err
}
func (e *PermanentError) Is(target error) bool {
_, ok := target.(*PermanentError)
return ok
}
// Permanent wraps the given err in a *PermanentError.
func Permanent(err error) error {
if err == nil {
return nil
}
return &PermanentError{
Err: err,
}
}

View file

@ -1,6 +1,7 @@
package backoff package backoff
import ( import (
"context"
"sync" "sync"
"time" "time"
) )
@ -13,7 +14,8 @@ type Ticker struct {
C <-chan time.Time C <-chan time.Time
c chan time.Time c chan time.Time
b BackOff b BackOff
timer timer ctx context.Context
timer Timer
stop chan struct{} stop chan struct{}
stopOnce sync.Once stopOnce sync.Once
} }
@ -25,12 +27,22 @@ type Ticker struct {
// provided backoff policy (notably calling NextBackOff or Reset) // provided backoff policy (notably calling NextBackOff or Reset)
// while the ticker is running. // while the ticker is running.
func NewTicker(b BackOff) *Ticker { func NewTicker(b BackOff) *Ticker {
return NewTickerWithTimer(b, &defaultTimer{})
}
// NewTickerWithTimer returns a new Ticker with a custom timer.
// A default timer that uses system timer is used when nil is passed.
func NewTickerWithTimer(b BackOff, timer Timer) *Ticker {
if timer == nil {
timer = &defaultTimer{}
}
c := make(chan time.Time) c := make(chan time.Time)
t := &Ticker{ t := &Ticker{
C: c, C: c,
c: c, c: c,
b: b, b: b,
timer: &defaultTimer{}, ctx: getContext(b),
timer: timer,
stop: make(chan struct{}), stop: make(chan struct{}),
} }
t.b.Reset() t.b.Reset()
@ -61,6 +73,8 @@ func (t *Ticker) run() {
case <-t.stop: case <-t.stop:
t.c = nil // Prevent future ticks from being sent to the channel. t.c = nil // Prevent future ticks from being sent to the channel.
return return
case <-t.ctx.Done():
return
} }
} }
} }

View file

@ -2,7 +2,7 @@ package backoff
import "time" import "time"
type timer interface { type Timer interface {
Start(duration time.Duration) Start(duration time.Duration)
Stop() Stop()
C() <-chan time.Time C() <-chan time.Time

38
vendor/github.com/cenkalti/backoff/v4/tries.go generated vendored Normal file
View file

@ -0,0 +1,38 @@
package backoff
import "time"
/*
WithMaxRetries creates a wrapper around another BackOff, which will
return Stop if NextBackOff() has been called too many times since
the last time Reset() was called
Note: Implementation is not thread-safe.
*/
func WithMaxRetries(b BackOff, max uint64) BackOff {
return &backOffTries{delegate: b, maxTries: max}
}
type backOffTries struct {
delegate BackOff
maxTries uint64
numTries uint64
}
func (b *backOffTries) NextBackOff() time.Duration {
if b.maxTries == 0 {
return Stop
}
if b.maxTries > 0 {
if b.maxTries <= b.numTries {
return Stop
}
b.numTries++
}
return b.delegate.NextBackOff()
}
func (b *backOffTries) Reset() {
b.numTries = 0
b.delegate.Reset()
}

View file

@ -1,29 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [5.0.0] - 2024-12-19
### Added
- RetryAfterError can be returned from an operation to indicate how long to wait before the next retry.
### Changed
- Retry function now accepts additional options for specifying max number of tries and max elapsed time.
- Retry function now accepts a context.Context.
- Operation function signature changed to return result (any type) and error.
### Removed
- RetryNotify* and RetryWithData functions. Only single Retry function remains.
- Optional arguments from ExponentialBackoff constructor.
- Clock and Timer interfaces.
### Fixed
- The original error is returned from Retry if there's a PermanentError. (#144)
- The Retry function respects the wrapped PermanentError. (#140)

View file

@ -1,46 +0,0 @@
package backoff
import (
"fmt"
"time"
)
// PermanentError signals that the operation should not be retried.
type PermanentError struct {
Err error
}
// Permanent wraps the given err in a *PermanentError.
func Permanent(err error) error {
if err == nil {
return nil
}
return &PermanentError{
Err: err,
}
}
// Error returns a string representation of the Permanent error.
func (e *PermanentError) Error() string {
return e.Err.Error()
}
// Unwrap returns the wrapped error.
func (e *PermanentError) Unwrap() error {
return e.Err
}
// RetryAfterError signals that the operation should be retried after the given duration.
type RetryAfterError struct {
Duration time.Duration
}
// RetryAfter returns a RetryAfter error that specifies how long to wait before retrying.
func RetryAfter(seconds int) error {
return &RetryAfterError{Duration: time.Duration(seconds) * time.Second}
}
// Error returns a string representation of the RetryAfter error.
func (e *RetryAfterError) Error() string {
return fmt.Sprintf("retry after %s", e.Duration)
}

View file

@ -1,139 +0,0 @@
package backoff
import (
"context"
"errors"
"time"
)
// DefaultMaxElapsedTime sets a default limit for the total retry duration.
const DefaultMaxElapsedTime = 15 * time.Minute
// Operation is a function that attempts an operation and may be retried.
type Operation[T any] func() (T, error)
// Notify is a function called on operation error with the error and backoff duration.
type Notify func(error, time.Duration)
// retryOptions holds configuration settings for the retry mechanism.
type retryOptions struct {
BackOff BackOff // Strategy for calculating backoff periods.
Timer timer // Timer to manage retry delays.
Notify Notify // Optional function to notify on each retry error.
MaxTries uint // Maximum number of retry attempts.
MaxElapsedTime time.Duration // Maximum total time for all retries.
}
type RetryOption func(*retryOptions)
// WithBackOff configures a custom backoff strategy.
func WithBackOff(b BackOff) RetryOption {
return func(args *retryOptions) {
args.BackOff = b
}
}
// withTimer sets a custom timer for managing delays between retries.
func withTimer(t timer) RetryOption {
return func(args *retryOptions) {
args.Timer = t
}
}
// WithNotify sets a notification function to handle retry errors.
func WithNotify(n Notify) RetryOption {
return func(args *retryOptions) {
args.Notify = n
}
}
// WithMaxTries limits the number of all attempts.
func WithMaxTries(n uint) RetryOption {
return func(args *retryOptions) {
args.MaxTries = n
}
}
// WithMaxElapsedTime limits the total duration for retry attempts.
func WithMaxElapsedTime(d time.Duration) RetryOption {
return func(args *retryOptions) {
args.MaxElapsedTime = d
}
}
// Retry attempts the operation until success, a permanent error, or backoff completion.
// It ensures the operation is executed at least once.
//
// Returns the operation result or error if retries are exhausted or context is cancelled.
func Retry[T any](ctx context.Context, operation Operation[T], opts ...RetryOption) (T, error) {
// Initialize default retry options.
args := &retryOptions{
BackOff: NewExponentialBackOff(),
Timer: &defaultTimer{},
MaxElapsedTime: DefaultMaxElapsedTime,
}
// Apply user-provided options to the default settings.
for _, opt := range opts {
opt(args)
}
defer args.Timer.Stop()
startedAt := time.Now()
args.BackOff.Reset()
for numTries := uint(1); ; numTries++ {
// Execute the operation.
res, err := operation()
if err == nil {
return res, nil
}
// Stop retrying if maximum tries exceeded.
if args.MaxTries > 0 && numTries >= args.MaxTries {
return res, err
}
// Handle permanent errors without retrying.
var permanent *PermanentError
if errors.As(err, &permanent) {
return res, permanent.Unwrap()
}
// Stop retrying if context is cancelled.
if cerr := context.Cause(ctx); cerr != nil {
return res, cerr
}
// Calculate next backoff duration.
next := args.BackOff.NextBackOff()
if next == Stop {
return res, err
}
// Reset backoff if RetryAfterError is encountered.
var retryAfter *RetryAfterError
if errors.As(err, &retryAfter) {
next = retryAfter.Duration
args.BackOff.Reset()
}
// Stop retrying if maximum elapsed time exceeded.
if args.MaxElapsedTime > 0 && time.Since(startedAt)+next > args.MaxElapsedTime {
return res, err
}
// Notify on error if a notifier function is provided.
if args.Notify != nil {
args.Notify(err, next)
}
// Wait for the next backoff period or context cancellation.
args.Timer.Start(next)
select {
case <-args.Timer.C():
case <-ctx.Done():
return res, context.Cause(ctx)
}
}
}

View file

@ -1,6 +1,5 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2017-2024 Ludovic Fernandez
Copyright (c) 2015-2017 Sebastian Erhart Copyright (c) 2015-2017 Sebastian Erhart
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy

View file

@ -13,11 +13,10 @@ type AccountService service
// New Creates a new account. // New Creates a new account.
func (a *AccountService) New(req acme.Account) (acme.ExtendedAccount, error) { func (a *AccountService) New(req acme.Account) (acme.ExtendedAccount, error) {
var account acme.Account var account acme.Account
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)
} }
@ -30,9 +29,9 @@ func (a *AccountService) New(req acme.Account) (acme.ExtendedAccount, error) {
// NewEAB Creates a new account with an External Account Binding. // NewEAB Creates a new account with an External Account Binding.
func (a *AccountService) NewEAB(accMsg acme.Account, kid, hmacEncoded string) (acme.ExtendedAccount, error) { func (a *AccountService) NewEAB(accMsg acme.Account, kid, hmacEncoded string) (acme.ExtendedAccount, error) {
hmac, err := decodeEABHmac(hmacEncoded) hmac, err := base64.RawURLEncoding.DecodeString(hmacEncoded)
if err != nil { if err != nil {
return acme.ExtendedAccount{}, err return acme.ExtendedAccount{}, fmt.Errorf("acme: could not decode hmac key: %w", err)
} }
eabJWS, err := a.core.signEABContent(a.core.GetDirectory().NewAccountURL, kid, hmac) eabJWS, err := a.core.signEABContent(a.core.GetDirectory().NewAccountURL, kid, hmac)
@ -52,12 +51,10 @@ func (a *AccountService) Get(accountURL string) (acme.Account, error) {
} }
var account acme.Account var account acme.Account
_, err := a.core.postAsGet(accountURL, &account) _, err := a.core.postAsGet(accountURL, &account)
if err != nil { if err != nil {
return acme.Account{}, err return acme.Account{}, err
} }
return account, nil return account, nil
} }
@ -68,7 +65,6 @@ func (a *AccountService) Update(accountURL string, req acme.Account) (acme.Accou
} }
var account acme.Account var account acme.Account
_, err := a.core.post(accountURL, req, &account) _, err := a.core.post(accountURL, req, &account)
if err != nil { if err != nil {
return acme.Account{}, err return acme.Account{}, err
@ -85,20 +81,5 @@ func (a *AccountService) Deactivate(accountURL string) error {
req := acme.Account{Status: acme.StatusDeactivated} req := acme.Account{Status: acme.StatusDeactivated}
_, err := a.core.post(accountURL, req, nil) _, err := a.core.post(accountURL, req, nil)
return err return err
} }
func decodeEABHmac(hmacEncoded string) ([]byte, error) {
hmac, errRaw := base64.RawURLEncoding.DecodeString(hmacEncoded)
if errRaw == nil {
return hmac, nil
}
hmac, err := base64.URLEncoding.DecodeString(hmacEncoded)
if err == nil {
return hmac, nil
}
return nil, fmt.Errorf("acme: could not decode hmac key: %w", errors.Join(errRaw, err))
}

View file

@ -10,7 +10,7 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/cenkalti/backoff/v5" "github.com/cenkalti/backoff/v4"
"github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme"
"github.com/go-acme/lego/v4/acme/api/internal/nonces" "github.com/go-acme/lego/v4/acme/api/internal/nonces"
"github.com/go-acme/lego/v4/acme/api/internal/secure" "github.com/go-acme/lego/v4/acme/api/internal/secure"
@ -61,7 +61,7 @@ func New(httpClient *http.Client, userAgent, caDirURL, kid string, privateKey cr
// post performs an HTTP POST request and parses the response body as JSON, // post performs an HTTP POST request and parses the response body as JSON,
// into the provided respBody object. // into the provided respBody object.
func (a *Core) post(uri string, reqBody, response any) (*http.Response, error) { func (a *Core) post(uri string, reqBody, response interface{}) (*http.Response, error) {
content, err := json.Marshal(reqBody) content, err := json.Marshal(reqBody)
if err != nil { if err != nil {
return nil, errors.New("failed to marshal message") return nil, errors.New("failed to marshal message")
@ -71,51 +71,57 @@ func (a *Core) post(uri string, reqBody, response any) (*http.Response, error) {
} }
// 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 any) (*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)
} }
func (a *Core) retrievablePost(uri string, content []byte, response any) (*http.Response, error) { func (a *Core) retrievablePost(uri string, content []byte, response interface{}) (*http.Response, error) {
ctx := context.Background()
// during tests, allow to support ~90% of bad nonce with a minimum of attempts. // during tests, allow to support ~90% of bad nonce with a minimum of attempts.
bo := backoff.NewExponentialBackOff() bo := backoff.NewExponentialBackOff()
bo.InitialInterval = 200 * time.Millisecond bo.InitialInterval = 200 * time.Millisecond
bo.MaxInterval = 5 * time.Second bo.MaxInterval = 5 * time.Second
bo.MaxElapsedTime = 20 * time.Second
operation := func() (*http.Response, error) { ctx, cancel := context.WithCancel(context.Background())
resp, err := a.signedPost(uri, content, response)
var resp *http.Response
operation := func() error {
var err error
resp, err = a.signedPost(uri, content, response)
if err != nil { if err != nil {
// Retry if the nonce was invalidated // Retry if the nonce was invalidated
var e *acme.NonceError var e *acme.NonceError
if errors.As(err, &e) { if errors.As(err, &e) {
return resp, err return err
} }
return resp, backoff.Permanent(err) cancel()
return err
} }
return resp, nil return nil
} }
notify := func(err error, duration time.Duration) { notify := func(err error, duration time.Duration) {
log.Infof("retry due to: %v", err) log.Infof("retry due to: %v", err)
} }
return backoff.Retry(ctx, operation, err := backoff.RetryNotify(operation, backoff.WithContext(bo, ctx), notify)
backoff.WithBackOff(bo), if err != nil {
backoff.WithMaxElapsedTime(20*time.Second), return resp, err
backoff.WithNotify(notify)) }
return resp, nil
} }
func (a *Core) signedPost(uri string, content []byte, response any) (*http.Response, error) { func (a *Core) signedPost(uri string, content []byte, response interface{}) (*http.Response, error) {
signedContent, err := a.jws.SignContent(uri, content) signedContent, err := a.jws.SignContent(uri, content)
if err != nil { if err != nil {
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)
@ -155,7 +161,6 @@ func getDirectory(do *sender.Doer, caDirURL string) (acme.Directory, error) {
if dir.NewAccountURL == "" { if dir.NewAccountURL == "" {
return dir, errors.New("directory missing new registration URL") return dir, errors.New("directory missing new registration URL")
} }
if dir.NewOrderURL == "" { if dir.NewOrderURL == "" {
return dir, errors.New("directory missing new order URL") return dir, errors.New("directory missing new order URL")
} }

View file

@ -15,12 +15,10 @@ func (c *AuthorizationService) Get(authzURL string) (acme.Authorization, error)
} }
var authz acme.Authorization var authz acme.Authorization
_, err := c.core.postAsGet(authzURL, &authz) _, err := c.core.postAsGet(authzURL, &authz)
if err != nil { if err != nil {
return acme.Authorization{}, err return acme.Authorization{}, err
} }
return authz, nil return authz, nil
} }
@ -31,8 +29,6 @@ func (c *AuthorizationService) Deactivate(authzURL string) error {
} }
var disabledAuth acme.Authorization var disabledAuth acme.Authorization
_, err := c.core.post(authzURL, acme.Authorization{Status: acme.StatusDeactivated}, &disabledAuth) _, err := c.core.post(authzURL, acme.Authorization{Status: acme.StatusDeactivated}, &disabledAuth)
return err return err
} }

View file

@ -1,13 +1,15 @@
package api package api
import ( import (
"bytes" "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"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/log"
) )
// maxBodySize is the maximum size of body that we will read. // maxBodySize is the maximum size of body that we will read.
@ -37,7 +39,7 @@ func (c *CertificateService) GetAll(certURL string, bundle bool) (map[string]*ac
certs := map[string]*acme.RawCertificate{certURL: cert} certs := map[string]*acme.RawCertificate{certURL: cert}
// URLs of "alternate" link relation // URLs of "alternate" link relation
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4.2 // - https://tools.ietf.org/html/rfc8555#section-7.4.2
alts := getLinks(headers, "alternate") alts := getLinks(headers, "alternate")
for _, alt := range alts { for _, alt := range alts {
@ -69,27 +71,62 @@ func (c *CertificateService) get(certURL string, bundle bool) (*acme.RawCertific
return nil, nil, err return nil, nil, err
} }
data, err := io.ReadAll(http.MaxBytesReader(nil, resp.Body, maxBodySize)) data, err := ioutil.ReadAll(http.MaxBytesReader(nil, resp.Body, maxBodySize))
if err != nil { if err != nil {
return nil, resp.Header, err return nil, resp.Header, err
} }
cert := c.getCertificateChain(data, bundle) cert := c.getCertificateChain(data, resp.Header, bundle, certURL)
return cert, resp.Header, err return cert, resp.Header, err
} }
// getCertificateChain Returns the certificate and the issuer certificate. // getCertificateChain Returns the certificate and the issuer certificate.
func (c *CertificateService) getCertificateChain(cert []byte, bundle bool) *acme.RawCertificate { 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 {
return &acme.RawCertificate{Cert: cert, Issuer: issuer}
}
// If bundle is false, we want to return a single certificate. // The issuer certificate link may be supplied via an "up" link
// To do this, we remove the issuer cert(s) from the issued cert. // in the response headers of a new certificate.
if !bundle { // See https://tools.ietf.org/html/rfc8555#section-7.4.2
cert = bytes.TrimSuffix(cert, issuer) up := getLink(headers, "up")
issuer, err := c.getIssuerFromLink(up)
if err != nil {
// 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)
} else if len(issuer) > 0 {
// If bundle is true, we want to return a certificate bundle.
// To do this, we append the issuer cert to the issued cert.
if bundle {
cert = append(cert, issuer...)
}
} }
return &acme.RawCertificate{Cert: cert, Issuer: issuer} return &acme.RawCertificate{Cert: cert, Issuer: issuer}
} }
// getIssuerFromLink requests the issuer certificate.
func (c *CertificateService) getIssuerFromLink(up string) ([]byte, error) {
if up == "" {
return nil, nil
}
log.Infof("acme: Requesting issuer cert from %s", up)
cert, _, err := c.get(up, false)
if err != nil {
return nil, err
}
_, err = x509.ParseCertificate(cert.Cert)
if err != nil {
return nil, err
}
return certcrypto.PEMEncode(certcrypto.DERCertificateBytes(cert.Cert)), nil
}

View file

@ -17,7 +17,6 @@ func (c *ChallengeService) New(chlgURL string) (acme.ExtendedChallenge, error) {
// Challenge initiation is done by sending a JWS payload containing the trivial JSON object `{}`. // Challenge initiation is done by sending a JWS payload containing the trivial JSON object `{}`.
// We use an empty struct instance as the postJSON payload here to achieve this result. // We use an empty struct instance as the postJSON payload here to achieve this result.
var chlng acme.ExtendedChallenge var chlng acme.ExtendedChallenge
resp, err := c.core.post(chlgURL, struct{}{}, &chlng) resp, err := c.core.post(chlgURL, struct{}{}, &chlng)
if err != nil { if err != nil {
return acme.ExtendedChallenge{}, err return acme.ExtendedChallenge{}, err
@ -25,7 +24,6 @@ func (c *ChallengeService) New(chlgURL string) (acme.ExtendedChallenge, error) {
chlng.AuthorizationURL = getLink(resp.Header, "up") chlng.AuthorizationURL = getLink(resp.Header, "up")
chlng.RetryAfter = getRetryAfter(resp) chlng.RetryAfter = getRetryAfter(resp)
return chlng, nil return chlng, nil
} }
@ -36,7 +34,6 @@ func (c *ChallengeService) Get(chlgURL string) (acme.ExtendedChallenge, error) {
} }
var chlng acme.ExtendedChallenge var chlng acme.ExtendedChallenge
resp, err := c.core.postAsGet(chlgURL, &chlng) resp, err := c.core.postAsGet(chlgURL, &chlng)
if err != nil { if err != nil {
return acme.ExtendedChallenge{}, err return acme.ExtendedChallenge{}, err
@ -44,6 +41,5 @@ func (c *ChallengeService) Get(chlgURL string) (acme.ExtendedChallenge, error) {
chlng.AuthorizationURL = getLink(resp.Header, "up") chlng.AuthorizationURL = getLink(resp.Header, "up")
chlng.RetryAfter = getRetryAfter(resp) chlng.RetryAfter = getRetryAfter(resp)
return chlng, nil return chlng, nil
} }

View file

@ -1,49 +0,0 @@
package api
import (
"cmp"
"maps"
"net"
"slices"
"github.com/go-acme/lego/v4/acme"
)
func createIdentifiers(domains []string) []acme.Identifier {
uniqIdentifiers := make(map[string]acme.Identifier)
for _, domain := range domains {
if _, ok := uniqIdentifiers[domain]; ok {
continue
}
ident := acme.Identifier{Value: domain, Type: "dns"}
if net.ParseIP(domain) != nil {
ident.Type = "ip"
}
uniqIdentifiers[domain] = ident
}
return slices.AppendSeq(make([]acme.Identifier, 0, len(uniqIdentifiers)), maps.Values(uniqIdentifiers))
}
// compareIdentifiers compares 2 slices of [acme.Identifier].
func compareIdentifiers(a, b []acme.Identifier) int {
// Clones slices to avoid modifying original slices.
right := slices.Clone(a)
left := slices.Clone(b)
slices.SortStableFunc(right, compareIdentifier)
slices.SortStableFunc(left, compareIdentifier)
return slices.CompareFunc(right, left, compareIdentifier)
}
func compareIdentifier(right, left acme.Identifier) int {
return cmp.Or(
cmp.Compare(right.Type, left.Type),
cmp.Compare(right.Value, left.Value),
)
}

View file

@ -11,11 +11,10 @@ import (
// Manager Manages nonces. // Manager Manages nonces.
type Manager struct { type Manager struct {
sync.Mutex
do *sender.Doer do *sender.Doer
nonceURL string nonceURL string
nonces []string nonces []string
sync.Mutex
} }
// NewManager Creates a new Manager. // NewManager Creates a new Manager.
@ -37,7 +36,6 @@ func (n *Manager) Pop() (string, bool) {
nonce := n.nonces[len(n.nonces)-1] nonce := n.nonces[len(n.nonces)-1]
n.nonces = n.nonces[:len(n.nonces)-1] n.nonces = n.nonces[:len(n.nonces)-1]
return nonce, true return nonce, true
} }
@ -45,7 +43,6 @@ func (n *Manager) Pop() (string, bool) {
func (n *Manager) Push(nonce string) { func (n *Manager) Push(nonce string) {
n.Lock() n.Lock()
defer n.Unlock() defer n.Unlock()
n.nonces = append(n.nonces, nonce) n.nonces = append(n.nonces, nonce)
} }
@ -54,7 +51,6 @@ func (n *Manager) Nonce() (string, error) {
if nonce, ok := n.Pop(); ok { if nonce, ok := n.Pop(); ok {
return nonce, nil return nonce, nil
} }
return n.getNonce() return n.getNonce()
} }
@ -67,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.
@ -36,7 +36,6 @@ func (j *JWS) SetKid(kid string) {
// SignContent Signs a content with the JWS. // SignContent Signs a content with the JWS.
func (j *JWS) SignContent(url string, content []byte) (*jose.JSONWebSignature, error) { func (j *JWS) SignContent(url string, content []byte) (*jose.JSONWebSignature, error) {
var alg jose.SignatureAlgorithm var alg jose.SignatureAlgorithm
switch k := j.privKey.(type) { switch k := j.privKey.(type) {
case *rsa.PrivateKey: case *rsa.PrivateKey:
alg = jose.RS256 alg = jose.RS256
@ -55,7 +54,7 @@ func (j *JWS) SignContent(url string, content []byte) (*jose.JSONWebSignature, e
options := jose.SignerOptions{ options := jose.SignerOptions{
NonceSource: j.nonces, NonceSource: j.nonces,
ExtraHeaders: map[jose.HeaderKey]any{ ExtraHeaders: map[jose.HeaderKey]interface{}{
"url": url, "url": url,
}, },
} }
@ -73,14 +72,12 @@ func (j *JWS) SignContent(url string, content []byte) (*jose.JSONWebSignature, e
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to sign content: %w", err) return nil, fmt.Errorf("failed to sign content: %w", err)
} }
return signed, nil return signed, nil
} }
// SignEABContent Signs an external account binding content with the JWS. // SignEABContent Signs an external account binding content with the JWS.
func (j *JWS) SignEABContent(url, kid string, hmac []byte) (*jose.JSONWebSignature, error) { func (j *JWS) SignEABContent(url, kid string, hmac []byte) (*jose.JSONWebSignature, error) {
jwk := jose.JSONWebKey{Key: j.privKey} jwk := jose.JSONWebKey{Key: j.privKey}
jwkJSON, err := jwk.Public().MarshalJSON() jwkJSON, err := jwk.Public().MarshalJSON()
if err != nil { if err != nil {
return nil, fmt.Errorf("acme: error encoding eab jwk key: %w", err) return nil, fmt.Errorf("acme: error encoding eab jwk key: %w", err)
@ -90,7 +87,7 @@ func (j *JWS) SignEABContent(url, kid string, hmac []byte) (*jose.JSONWebSignatu
jose.SigningKey{Algorithm: jose.HS256, Key: hmac}, jose.SigningKey{Algorithm: jose.HS256, Key: hmac},
&jose.SignerOptions{ &jose.SignerOptions{
EmbedJWK: false, EmbedJWK: false,
ExtraHeaders: map[jose.HeaderKey]any{ ExtraHeaders: map[jose.HeaderKey]interface{}{
"kid": kid, "kid": kid,
"url": url, "url": url,
}, },
@ -111,7 +108,6 @@ func (j *JWS) SignEABContent(url, kid string, hmac []byte) (*jose.JSONWebSignatu
// GetKeyAuthorization Gets the key authorization for a token. // GetKeyAuthorization Gets the key authorization for a token.
func (j *JWS) GetKeyAuthorization(token string) (string, error) { func (j *JWS) GetKeyAuthorization(token string) (string, error) {
var publicKey crypto.PublicKey var publicKey crypto.PublicKey
switch k := j.privKey.(type) { switch k := j.privKey.(type) {
case *ecdsa.PrivateKey: case *ecdsa.PrivateKey:
publicKey = k.Public() publicKey = k.Public()

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"
@ -27,8 +28,6 @@ type Doer struct {
// NewDoer Creates a new Doer. // NewDoer Creates a new Doer.
func NewDoer(client *http.Client, userAgent string) *Doer { func NewDoer(client *http.Client, userAgent string) *Doer {
client.Transport = newHTTPSOnly(client)
return &Doer{ return &Doer{
httpClient: client, httpClient: client,
userAgent: userAgent, userAgent: userAgent,
@ -37,7 +36,7 @@ func NewDoer(client *http.Client, userAgent string) *Doer {
// Get performs a GET request with a proper User-Agent string. // Get performs a GET request with a proper User-Agent string.
// If "response" is not provided, callers should close resp.Body when done reading from it. // If "response" is not provided, callers should close resp.Body when done reading from it.
func (d *Doer) Get(url string, response any) (*http.Response, error) { func (d *Doer) Get(url string, response interface{}) (*http.Response, error) {
req, err := d.newRequest(http.MethodGet, url, nil) req, err := d.newRequest(http.MethodGet, url, nil)
if err != nil { if err != nil {
return nil, err return nil, err
@ -59,7 +58,7 @@ func (d *Doer) Head(url string) (*http.Response, error) {
// Post performs a POST request with a proper User-Agent string. // Post performs a POST request with a proper User-Agent string.
// If "response" is not provided, callers should close resp.Body when done reading from it. // If "response" is not provided, callers should close resp.Body when done reading from it.
func (d *Doer) Post(url string, body io.Reader, bodyType string, response any) (*http.Response, error) { func (d *Doer) Post(url string, body io.Reader, bodyType string, response interface{}) (*http.Response, error) {
req, err := d.newRequest(http.MethodPost, url, body, contentType(bodyType)) req, err := d.newRequest(http.MethodPost, url, body, contentType(bodyType))
if err != nil { if err != nil {
return nil, err return nil, err
@ -86,7 +85,7 @@ func (d *Doer) newRequest(method, uri string, body io.Reader, opts ...RequestOpt
return req, nil return req, nil
} }
func (d *Doer) do(req *http.Request, response any) (*http.Response, error) { func (d *Doer) do(req *http.Request, response interface{}) (*http.Response, error) {
resp, err := d.httpClient.Do(req) resp, err := d.httpClient.Do(req)
if err != nil { if err != nil {
return nil, err return nil, err
@ -97,7 +96,7 @@ func (d *Doer) do(req *http.Request, response any) (*http.Response, error) {
} }
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
} }
@ -121,13 +120,12 @@ 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)
} }
var errorDetails *acme.ProblemDetails var errorDetails *acme.ProblemDetails
err = json.Unmarshal(body, &errorDetails) err = json.Unmarshal(body, &errorDetails)
if err != nil { if err != nil {
return fmt.Errorf("%d ::%s :: %s :: %w :: %s", resp.StatusCode, req.Method, req.URL, err, string(body)) return fmt.Errorf("%d ::%s :: %s :: %w :: %s", resp.StatusCode, req.Method, req.URL, err, string(body))
@ -136,46 +134,12 @@ 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}
} }
if errorDetails.HTTPStatus == http.StatusConflict && errorDetails.Type == acme.AlreadyReplacedErr {
return &acme.AlreadyReplacedError{ProblemDetails: errorDetails}
}
return errorDetails return errorDetails
} }
return nil return nil
} }
type httpsOnly struct {
rt http.RoundTripper
}
func newHTTPSOnly(client *http.Client) *httpsOnly {
if client.Transport == nil {
return &httpsOnly{rt: http.DefaultTransport}
}
return &httpsOnly{rt: client.Transport}
}
// RoundTrip ensure HTTPS is used.
// Each ACME function is accomplished by the client sending a sequence of HTTPS requests to the server [RFC2818],
// carrying JSON messages [RFC8259].
// Use of HTTPS is REQUIRED.
// https://datatracker.ietf.org/doc/html/rfc8555#section-6.1
func (r *httpsOnly) RoundTrip(req *http.Request) (*http.Response, error) {
if req.URL.Scheme != "https" {
return nil, fmt.Errorf("HTTPS is required: %s", req.URL)
}
return r.rt.RoundTrip(req)
}

View file

@ -1,10 +1,11 @@
// Code generated by 'internal/releaser'; DO NOT EDIT.
package sender package sender
// CODE GENERATED AUTOMATICALLY
// THIS FILE MUST NOT BE EDITED BY HAND
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.29.0" ourUserAgent = "xenolf-acme/4.4.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,98 +3,27 @@ package api
import ( import (
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt"
"slices"
"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 the profile
// which will be used to affect issuance of the certificate requested by this Order.
// - https://www.ietf.org/id/draft-ietf-acme-profiles-00.html#section-4
Profile string
// A string uniquely identifying a previously-issued certificate which this
// order is intended to replace.
// - https://www.rfc-editor.org/rfc/rfc9773.html#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) var identifiers []acme.Identifier
} for _, domain := range domains {
identifiers = append(identifiers, acme.Identifier{Type: "dns", Value: domain})
// NewWithOptions Creates a new order.
func (o *OrderService) NewWithOptions(domains []string, opts *OrderOptions) (acme.ExtendedOrder, error) {
orderReq := acme.Order{Identifiers: createIdentifiers(domains)}
if opts != nil {
if !opts.NotAfter.IsZero() {
orderReq.NotAfter = opts.NotAfter.Format(time.RFC3339)
} }
if !opts.NotBefore.IsZero() { orderReq := acme.Order{Identifiers: identifiers}
orderReq.NotBefore = opts.NotBefore.Format(time.RFC3339)
}
if o.core.GetDirectory().RenewalInfo != "" {
orderReq.Replaces = opts.ReplacesCertID
}
if opts.Profile != "" {
orderReq.Profile = opts.Profile
}
}
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 {
are := &acme.AlreadyReplacedError{}
if !errors.As(err, &are) {
return acme.ExtendedOrder{}, err return acme.ExtendedOrder{}, err
} }
// If the Server rejects the request because the identified certificate has already been marked as replaced,
// it MUST return an HTTP 409 (Conflict) with a problem document of type "alreadyReplaced" (see Section 7.4).
// https://www.rfc-editor.org/rfc/rfc9773.html#section-5
orderReq.Replaces = ""
resp, err = o.core.post(o.core.GetDirectory().NewOrderURL, orderReq, &order)
if err != nil {
return acme.ExtendedOrder{}, err
}
}
// The server MUST return an error if it cannot fulfill the request as specified,
// and it MUST NOT issue a certificate with contents other than those requested.
// If the server requires the request to be modified in a certain way,
// it should indicate the required changes using an appropriate error type and description.
// https://www.rfc-editor.org/rfc/rfc8555#section-7.4
//
// Some ACME servers don't return an error,
// and/or change the order identifiers in the response,
// so we need to ensure that the identifiers are the same as requested.
// Deduplication by the server is allowed.
if compareIdentifiers(orderReq.Identifiers, order.Identifiers) != 0 {
// Sorts identifiers to avoid error message ambiguities about the order of the identifiers.
slices.SortStableFunc(orderReq.Identifiers, compareIdentifier)
slices.SortStableFunc(order.Identifiers, compareIdentifier)
return acme.ExtendedOrder{},
fmt.Errorf("order identifiers have been modified by the ACME server (RFC8555 §7.4): %+v != %+v",
orderReq.Identifiers, order.Identifiers)
}
return acme.ExtendedOrder{ return acme.ExtendedOrder{
Order: order, Order: order,
Location: resp.Header.Get("Location"), Location: resp.Header.Get("Location"),
@ -108,7 +37,6 @@ func (o *OrderService) Get(orderURL string) (acme.ExtendedOrder, error) {
} }
var order acme.Order var order acme.Order
_, err := o.core.postAsGet(orderURL, &order) _, err := o.core.postAsGet(orderURL, &order)
if err != nil { if err != nil {
return acme.ExtendedOrder{}, err return acme.ExtendedOrder{}, err
@ -124,14 +52,13 @@ 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) _, err := o.core.post(orderURL, csrMsg, &order)
if err != nil { if err != nil {
return acme.ExtendedOrder{}, err return acme.ExtendedOrder{}, err
} }
if order.Status == acme.StatusInvalid { if order.Status == acme.StatusInvalid {
return acme.ExtendedOrder{}, fmt.Errorf("invalid order: %w", order.Err()) return acme.ExtendedOrder{}, order.Error
} }
return acme.ExtendedOrder{Order: order}, nil return acme.ExtendedOrder{Order: order}, 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://www.rfc-editor.org/rfc/rfc9773.html
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

@ -23,13 +23,11 @@ func getLinks(header http.Header, rel string) []string {
linkExpr := regexp.MustCompile(`<(.+?)>(?:;[^;]+)*?;\s*rel="(.+?)"`) linkExpr := regexp.MustCompile(`<(.+?)>(?:;[^;]+)*?;\s*rel="(.+?)"`)
var links []string var links []string
for _, link := range header["Link"] { for _, link := range header["Link"] {
for _, m := range linkExpr.FindAllStringSubmatch(link, -1) { for _, m := range linkExpr.FindAllStringSubmatch(link, -1) {
if len(m) != 3 { if len(m) != 3 {
continue continue
} }
if m[2] == rel { if m[2] == rel {
links = append(links, m[1]) links = append(links, m[1])
} }

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://www.rfc-editor.org/rfc/rfc9773.html
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,33 +52,27 @@ 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"`
// profiles (optional, object):
// A map of profile names to human-readable descriptions of those profiles.
// https://www.ietf.org/id/draft-ietf-acme-profiles-00.html#section-3
Profiles map[string]string `json:"profiles"`
} }
// 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`
Location string `json:"-"` Location string `json:"-"`
} }
// 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):
@ -137,7 +112,7 @@ type ExtendedOrder struct {
} }
// 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.
@ -154,12 +129,6 @@ type Order struct {
// An array of identifier objects that the order pertains to. // An array of identifier objects that the order pertains to.
Identifiers []Identifier `json:"identifiers"` Identifiers []Identifier `json:"identifiers"`
// profile (string, optional):
// A string uniquely identifying the profile
// which will be used to affect issuance of the certificate requested by this Order.
// https://www.ietf.org/id/draft-ietf-acme-profiles-00.html#section-4
Profile string `json:"profile,omitempty"`
// notBefore (optional, string): // notBefore (optional, string):
// The requested value of the notBefore field in the certificate, // The requested value of the notBefore field in the certificate,
// in the date format defined in [RFC3339]. // in the date format defined in [RFC3339].
@ -193,24 +162,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://www.rfc-editor.org/rfc/rfc9773.html#section-5
Replaces string `json:"replaces,omitempty"`
}
func (r *Order) Err() error {
if r.Error != nil {
return r.Error
}
return nil
} }
// 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.
@ -221,11 +176,11 @@ type Authorization struct {
// The timestamp after which the server will consider this authorization invalid, // The timestamp after which the server will consider this authorization invalid,
// encoded in the format specified in RFC 3339 [RFC3339]. // encoded in the format specified in RFC 3339 [RFC3339].
// This field is REQUIRED for objects with "valid" in the "status" field. // This field is REQUIRED for objects with "valid" in the "status" field.
Expires time.Time `json:"expires,omitzero"` Expires time.Time `json:"expires,omitempty"`
// identifier (required, object): // identifier (required, object):
// The identifier that the account is authorized to represent // The identifier that the account is authorized to represent
Identifier Identifier `json:"identifier"` Identifier Identifier `json:"identifier,omitempty"`
// challenges (required, array of objects): // challenges (required, array of objects):
// For pending authorizations, the challenges that the client can fulfill in order to prove possession of the identifier. // For pending authorizations, the challenges that the client can fulfill in order to prove possession of the identifier.
@ -245,7 +200,6 @@ type Authorization struct {
// ExtendedChallenge a extended Challenge. // ExtendedChallenge a extended Challenge.
type ExtendedChallenge struct { type ExtendedChallenge struct {
Challenge Challenge
// Contains the value of the response header `Retry-After` // Contains the value of the response header `Retry-After`
RetryAfter string `json:"-"` RetryAfter string `json:"-"`
// Contains the value of the response header `Link` rel="up" // Contains the value of the response header `Link` rel="up"
@ -253,8 +207,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.
@ -272,7 +226,7 @@ type Challenge struct {
// The time at which the server validated this challenge, // The time at which the server validated this challenge,
// encoded in the format specified in RFC 3339 [RFC3339]. // encoded in the format specified in RFC 3339 [RFC3339].
// This field is REQUIRED if the "status" field is "valid". // This field is REQUIRED if the "status" field is "valid".
Validated time.Time `json:"validated,omitzero"` Validated time.Time `json:"validated,omitempty"`
// error (optional, object): // error (optional, object):
// Error that occurred while the server was validating the challenge, if any, // Error that occurred while the server was validating the challenge, if any,
@ -287,31 +241,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"`
} }
func (c *Challenge) Err() error {
if c.Error != nil {
return c.Error
}
return nil
}
// 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].
@ -321,8 +267,8 @@ type CSRMessage struct {
} }
// 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.
@ -343,36 +289,3 @@ type RawCertificate struct {
Cert []byte Cert []byte
Issuer []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://www.rfc-editor.org/rfc/rfc9773.html
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://www.rfc-editor.org/rfc/rfc9773.html#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://www.rfc-editor.org/rfc/rfc9773.html#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

@ -2,19 +2,17 @@ package acme
import ( import (
"fmt" "fmt"
"strings"
) )
// Errors types. // Errors types.
const ( const (
errNS = "urn:ietf:params:acme:error:" errNS = "urn:ietf:params:acme:error:"
BadNonceErr = errNS + "badNonce" BadNonceErr = errNS + "badNonce"
AlreadyReplacedErr = errNS + "alreadyReplaced"
) )
// 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"`
@ -27,34 +25,30 @@ type ProblemDetails struct {
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
} }
func (p *ProblemDetails) Error() string {
var msg strings.Builder
msg.WriteString(fmt.Sprintf("acme: error: %d", p.HTTPStatus))
if p.Method != "" || p.URL != "" {
msg.WriteString(fmt.Sprintf(" :: %s :: %s", p.Method, p.URL))
}
msg.WriteString(fmt.Sprintf(" :: %s :: %s", p.Type, p.Detail))
for _, sub := range p.SubProblems {
msg.WriteString(fmt.Sprintf(", problem: %q :: %s", sub.Type, sub.Detail))
}
if p.Instance != "" {
msg.WriteString(", url: " + p.Instance)
}
return msg.String()
}
// 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"`
Identifier Identifier `json:"identifier"` Identifier Identifier `json:"identifier,omitempty"`
}
func (p ProblemDetails) Error() string {
msg := fmt.Sprintf("acme: error: %d", p.HTTPStatus)
if p.Method != "" || p.URL != "" {
msg += fmt.Sprintf(" :: %s :: %s", p.Method, p.URL)
}
msg += fmt.Sprintf(" :: %s :: %s", p.Type, p.Detail)
for _, sub := range p.SubProblems {
msg += fmt.Sprintf(", problem: %q :: %s", sub.Type, sub.Detail)
}
if p.Instance != "" {
msg += ", url: " + p.Instance
}
return msg
} }
// NonceError represents the error which is returned // NonceError represents the error which is returned
@ -62,10 +56,3 @@ type SubProblem struct {
type NonceError struct { type NonceError struct {
*ProblemDetails *ProblemDetails
} }
// AlreadyReplacedError represents the error which is returned
// If the Server rejects the request because the identified certificate has already been marked as replaced.
// - https://www.rfc-editor.org/rfc/rfc9773.html#section-5
type AlreadyReplacedError struct {
*ProblemDetails
}

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")
) )
@ -57,10 +54,8 @@ type DERCertificateBytes []byte
// ParsePEMBundle parses a certificate bundle from top to bottom and returns // ParsePEMBundle parses a certificate bundle from top to bottom and returns
// a slice of x509 certificates. This function will error if no certificates are found. // a slice of x509 certificates. This function will error if no certificates are found.
func ParsePEMBundle(bundle []byte) ([]*x509.Certificate, error) { func ParsePEMBundle(bundle []byte) ([]*x509.Certificate, error) {
var ( var certificates []*x509.Certificate
certificates []*x509.Certificate var certDERBlock *pem.Block
certDERBlock *pem.Block
)
for { for {
certDERBlock, bundle = pem.Decode(bundle) certDERBlock, bundle = pem.Decode(bundle)
@ -73,7 +68,6 @@ func ParsePEMBundle(bundle []byte) ([]*x509.Certificate, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
certificates = append(certificates, cert) certificates = append(certificates, cert)
} }
} }
@ -88,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)
@ -127,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:
@ -138,44 +127,13 @@ func GeneratePrivateKey(keyType KeyType) (crypto.PrivateKey, error) {
return nil, fmt.Errorf("invalid KeyType: %s", keyType) return nil, fmt.Errorf("invalid KeyType: %s", keyType)
} }
// Deprecated: uses [CreateCSR] instead.
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) {
return CreateCSR(privateKey, CSROptions{
Domain: domain,
SAN: san,
MustStaple: mustStaple,
})
}
type CSROptions struct {
Domain string
SAN []string
MustStaple bool
EmailAddresses []string
}
func CreateCSR(privateKey crypto.PrivateKey, opts CSROptions) ([]byte, error) {
var (
dnsNames []string
ipAddresses []net.IP
)
for _, altname := range opts.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: opts.Domain}, Subject: pkix.Name{CommonName: domain},
DNSNames: dnsNames, DNSNames: san,
EmailAddresses: opts.EmailAddresses,
IPAddresses: ipAddresses,
} }
if opts.MustStaple { if mustStaple {
template.ExtraExtensions = append(template.ExtraExtensions, pkix.Extension{ template.ExtraExtensions = append(template.ExtraExtensions, pkix.Extension{
Id: tlsFeatureExtensionOID, Id: tlsFeatureExtensionOID,
Value: ocspMustStapleFeature, Value: ocspMustStapleFeature,
@ -185,13 +143,12 @@ func CreateCSR(privateKey crypto.PrivateKey, opts CSROptions) ([]byte, error) {
return x509.CreateCertificateRequest(rand.Reader, &template, privateKey) return x509.CreateCertificateRequest(rand.Reader, &template, privateKey)
} }
func PEMEncode(data any) []byte { func PEMEncode(data interface{}) []byte {
return pem.EncodeToMemory(PEMBlock(data)) return pem.EncodeToMemory(PEMBlock(data))
} }
func PEMBlock(data any) *pem.Block { func PEMBlock(data interface{}) *pem.Block {
var pemBlock *pem.Block var pemBlock *pem.Block
switch key := data.(type) { switch key := data.(type) {
case *ecdsa.PrivateKey: case *ecdsa.PrivateKey:
keyBytes, _ := x509.MarshalECPrivateKey(key) keyBytes, _ := x509.MarshalECPrivateKey(key)
@ -241,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 != "" {
@ -272,17 +209,9 @@ func ExtractDomains(cert *x509.Certificate) []string {
if sanDomain == cert.Subject.CommonName { if sanDomain == cert.Subject.CommonName {
continue continue
} }
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
} }
@ -294,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
} }
@ -303,16 +232,18 @@ func ExtractDomainsCSR(csr *x509.CertificateRequest) []string {
domains = append(domains, sanName) domains = append(domains, sanName)
} }
cnip := net.ParseIP(csr.Subject.CommonName)
for _, sanIP := range csr.IPAddresses {
if !cnip.Equal(sanIP) {
domains = append(domains, sanIP.String())
}
}
return domains return domains
} }
func containsSAN(domains []string, sanName string) bool {
for _, existingName := range domains {
if existingName == sanName {
return true
}
}
return false
}
func GeneratePemCert(privateKey *rsa.PrivateKey, domain string, extensions []pkix.Extension) ([]byte, error) { func GeneratePemCert(privateKey *rsa.PrivateKey, domain string, extensions []pkix.Extension) ([]byte, error) {
derBytes, err := generateDerCert(privateKey, time.Time{}, domain, extensions) derBytes, err := generateDerCert(privateKey, time.Time{}, domain, extensions)
if err != nil { if err != nil {
@ -324,14 +255,13 @@ func GeneratePemCert(privateKey *rsa.PrivateKey, domain string, extensions []pki
func generateDerCert(privateKey *rsa.PrivateKey, expiration time.Time, domain string, extensions []pkix.Extension) ([]byte, error) { func generateDerCert(privateKey *rsa.PrivateKey, expiration time.Time, domain string, extensions []pkix.Extension) ([]byte, error) {
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if expiration.IsZero() { if expiration.IsZero() {
expiration = time.Now().AddDate(1, 0, 0) expiration = time.Now().Add(365)
} }
template := x509.Certificate{ template := x509.Certificate{
@ -344,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

@ -7,10 +7,18 @@ import (
"github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/log"
) )
const (
// overallRequestLimit is the overall number of request per second
// limited on the "new-reg", "new-authz" and "new-cert" endpoints.
// From the documentation the limitation is 20 requests per second,
// but using 20 as value doesn't work but 18 do.
overallRequestLimit = 18
)
func (c *Certifier) getAuthorizations(order acme.ExtendedOrder) ([]acme.Authorization, error) { func (c *Certifier) getAuthorizations(order acme.ExtendedOrder) ([]acme.Authorization, error) {
resc, errc := make(chan acme.Authorization), make(chan domainError) resc, errc := make(chan acme.Authorization), make(chan domainError)
delay := time.Second / time.Duration(c.overallRequestLimit) delay := time.Second / overallRequestLimit
for _, authzURL := range order.Authorizations { for _, authzURL := range order.Authorizations {
time.Sleep(delay) time.Sleep(delay)
@ -27,15 +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
} }
} }
@ -46,24 +52,28 @@ 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 {
log.Infof("Unable to get the authorization for %s: %v", authzURL, err) log.Infof("Unable to get the authorization for: %s", authzURL)
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
} }
log.Infof("Deactivating auth: %s", authzURL) log.Infof("Deactivating auth: %s", authzURL)
if c.core.Authorizations.Deactivate(authzURL) != nil { if c.core.Authorizations.Deactivate(authzURL) != nil {
log.Infof("Unable to deactivate the authorization: %s", authzURL) log.Infof("Unable to deactivate the authorization: %s", authzURL)
} }

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"
@ -22,17 +22,6 @@ import (
"golang.org/x/net/idna" "golang.org/x/net/idna"
) )
const (
// DefaultOverallRequestLimit is the overall number of request per second
// limited on the "new-reg", "new-authz" and "new-cert" endpoints.
// From the documentation the limitation is 20 requests per second,
// but using 20 as value doesn't work but 18 do.
// https://letsencrypt.org/docs/rate-limits/
// ZeroSSL has a limit of 7.
// https://help.zerossl.com/hc/en-us/articles/17864245480093-Advantages-over-Using-Let-s-Encrypt#h_01HT4Z1JCJFJQFJ1M3P7S085Q9
DefaultOverallRequestLimit = 18
)
// maxBodySize is the maximum size of body that we will read. // maxBodySize is the maximum size of body that we will read.
const maxBodySize = 1024 * 1024 const maxBodySize = 1024 * 1024
@ -60,61 +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
Bundle bool
PrivateKey crypto.PrivateKey PrivateKey crypto.PrivateKey
MustStaple bool MustStaple bool
EmailAddresses []string
NotBefore time.Time
NotAfter time.Time
Bundle bool
PreferredChain string PreferredChain string
// A string uniquely identifying the profile
// which will be used to affect issuance of the certificate requested by this Order.
// - https://www.ietf.org/id/draft-ietf-acme-profiles-00.html#section-4
Profile string
AlwaysDeactivateAuthorizations bool
// A string uniquely identifying a previously-issued certificate which this
// order is intended to replace.
// - https://www.rfc-editor.org/rfc/rfc9773.html#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
PrivateKey crypto.PrivateKey
NotBefore time.Time
NotAfter time.Time
Bundle bool Bundle bool
PreferredChain string PreferredChain string
// A string uniquely identifying the profile
// which will be used to affect issuance of the certificate requested by this Order.
// - https://www.ietf.org/id/draft-ietf-acme-profiles-00.html#section-4
Profile string
AlwaysDeactivateAuthorizations bool
// A string uniquely identifying a previously-issued certificate which this
// order is intended to replace.
// - https://www.rfc-editor.org/rfc/rfc9773.html#section-5
ReplacesCertID string
} }
type resolver interface { type resolver interface {
@ -124,8 +74,6 @@ type resolver interface {
type CertifierOptions struct { type CertifierOptions struct {
KeyType certcrypto.KeyType KeyType certcrypto.KeyType
Timeout time.Duration Timeout time.Duration
OverallRequestLimit int
DisableCommonName bool
} }
// Certifier A service to obtain/renew/revoke certificates. // Certifier A service to obtain/renew/revoke certificates.
@ -133,23 +81,15 @@ type Certifier struct {
core *api.Core core *api.Core
resolver resolver resolver resolver
options CertifierOptions options CertifierOptions
overallRequestLimit int
} }
// NewCertifier creates a Certifier. // NewCertifier creates a Certifier.
func NewCertifier(core *api.Core, resolver resolver, options CertifierOptions) *Certifier { func NewCertifier(core *api.Core, resolver resolver, options CertifierOptions) *Certifier {
c := &Certifier{ return &Certifier{
core: core, core: core,
resolver: resolver, resolver: resolver,
options: options, options: options,
} }
c.overallRequestLimit = options.OverallRequestLimit
if c.overallRequestLimit <= 0 {
c.overallRequestLimit = DefaultOverallRequestLimit
}
return c
} }
// Obtain tries to obtain a single certificate using all domains passed into it. // Obtain tries to obtain a single certificate using all domains passed into it.
@ -169,14 +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,
Profile: request.Profile,
ReplacesCertID: request.ReplacesCertID,
}
order, err := c.core.Orders.NewWithOptions(domains, orderOpts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -184,33 +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)
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.
@ -237,14 +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,
Profile: request.Profile,
ReplacesCertID: request.ReplacesCertID,
}
order, err := c.core.Orders.NewWithOptions(domains, orderOpts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -252,93 +178,72 @@ 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)
var privateKey []byte
if request.PrivateKey != nil {
privateKey = certcrypto.PEMEncode(request.PrivateKey)
}
cert, err := c.getForCSR(domains, order, request.Bundle, request.CSR.Raw, privateKey, 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, request ObtainRequest) (*Resource, error) { func (c *Certifier) getForOrder(domains []string, order acme.ExtendedOrder, bundle bool, privateKey crypto.PrivateKey, mustStaple bool, preferredChain string) (*Resource, error) {
privateKey := request.PrivateKey
if privateKey == nil { if privateKey == nil {
var err error var err error
privateKey, err = certcrypto.GeneratePrivateKey(c.options.KeyType) privateKey, err = certcrypto.GeneratePrivateKey(c.options.KeyType)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
commonName := "" // Determine certificate name(s) based on the authorization resources
if len(domains[0]) <= 64 && !c.options.DisableCommonName { 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)
} }
} }
csrOptions := certcrypto.CSROptions{ // TODO: should the CSR be customizable?
Domain: commonName, csr, err := certcrypto.GenerateCSR(privateKey, commonName, san, mustStaple)
SAN: san,
MustStaple: request.MustStaple,
EmailAddresses: request.EmailAddresses,
}
csr, err := certcrypto.CreateCSR(privateKey, csrOptions)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return c.getForCSR(domains, order, request.Bundle, csr, certcrypto.PEMEncode(privateKey), request.PreferredChain) return c.getForCSR(domains, order, bundle, csr, certcrypto.PEMEncode(privateKey), preferredChain)
} }
func (c *Certifier) getForCSR(domains []string, order acme.ExtendedOrder, bundle bool, csr, privateKeyPem []byte, preferredChain string) (*Resource, error) { func (c *Certifier) getForCSR(domains []string, order acme.ExtendedOrder, bundle bool, csr, privateKeyPem []byte, preferredChain string) (*Resource, error) {
@ -347,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
@ -443,11 +349,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
@ -460,28 +361,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
Profile string
AlwaysDeactivateAuthorizations bool
// Not supported for CSR request.
MustStaple bool
EmailAddresses []string
}
// 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.
@ -492,27 +376,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.
@ -539,18 +403,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.Profile = options.Profile
request.AlwaysDeactivateAuthorizations = options.AlwaysDeactivateAuthorizations
}
return c.ObtainForCSR(request)
} }
var privateKey crypto.PrivateKey var privateKey crypto.PrivateKey
@ -561,23 +418,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.EmailAddresses = options.EmailAddresses
request.Profile = options.Profile
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,
@ -618,7 +465,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
} }
@ -647,7 +494,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
} }
@ -678,13 +525,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,
@ -712,20 +554,19 @@ func checkOrderStatus(order acme.ExtendedOrder) (bool, error) {
case acme.StatusValid: case acme.StatusValid:
return true, nil return true, nil
case acme.StatusInvalid: case acme.StatusInvalid:
return false, fmt.Errorf("invalid order: %w", order.Err()) return false, order.Error
default: default:
return false, nil return false, nil
} }
} }
// 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 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]. // 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 {
sanitizedDomain, err := idna.ToASCII(domain) sanitizedDomain, err := idna.ToASCII(domain)
if err != nil { if err != nil {
@ -734,6 +575,5 @@ func sanitizeDomain(domains []string) []string {
sanitizedDomains = append(sanitizedDomains, sanitizedDomain) sanitizedDomains = append(sanitizedDomains, sanitizedDomain)
} }
} }
return sanitizedDomains return sanitizedDomains
} }

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,132 +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://www.rfc-editor.org/rfc/rfc9773.html#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 RFC 9773.
//
// - (4.1-11. Getting Renewal Information) https://www.rfc-editor.org/rfc/rfc9773.html
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.
rt := start
if window := end.Sub(start); window > 0 {
randomDuration := time.Duration(rand.Int63n(int64(window)))
rt = rt.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://www.rfc-editor.org/rfc/rfc9773.html
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 RFC 9773, 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")
) )
@ -40,6 +40,5 @@ func GetTargetedDomain(authz acme.Authorization) string {
if authz.Wildcard { if authz.Wildcard {
return "*." + authz.Identifier.Value return "*." + authz.Identifier.Value
} }
return authz.Identifier.Value return authz.Identifier.Value
} }

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"
@ -40,7 +39,6 @@ func CondOption(condition bool, opt ChallengeOption) ChallengeOption {
return nil return nil
} }
} }
return opt return opt
} }
@ -116,10 +114,9 @@ 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) {
case challenge.ProviderTimeout: case challenge.ProviderTimeout:
timeout, interval = provider.Timeout() timeout, interval = provider.Timeout()
@ -127,16 +124,15 @@ 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)
} }
return stop, errP return stop, errP
}) })
if err != nil { if err != nil {
@ -144,7 +140,6 @@ func (c *Challenge) Solve(authz acme.Authorization) error {
} }
chlng.KeyAuthorization = keyAuth chlng.KeyAuthorization = keyAuth
return c.validate(c.core, domain, chlng) return c.validate(c.core, domain, chlng)
} }
@ -169,7 +164,6 @@ func (c *Challenge) Sequential() (bool, time.Duration) {
if p, ok := c.provider.(sequential); ok { if p, ok := c.provider.(sequential); ok {
return ok, p.Sequential() return ok, p.Sequential()
} }
return false, 0 return false, 0
} }
@ -178,68 +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 return
cname := updateDomainWithCName(r, fqdn)
if cname == fqdn {
break
}
log.Infof("Found CNAME entry for %q: %q", fqdn, cname)
fqdn = cname
}
return fqdn
} }

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

@ -1,16 +1,12 @@
package dns01 package dns01
import (
"iter"
"github.com/miekg/dns"
)
// ToFqdn converts the name into a fqdn appending a trailing dot. // ToFqdn converts the name into a fqdn appending a trailing dot.
//
// Deprecated: Use [github.com/miekg/dns.Fqdn] directly.
func ToFqdn(name string) string { func ToFqdn(name string) string {
return dns.Fqdn(name) n := len(name)
if n == 0 || name[n-1] == '.' {
return name
}
return name + "."
} }
// UnFqdn converts the fqdn into a name removing the trailing dot. // UnFqdn converts the fqdn into a name removing the trailing dot.
@ -19,36 +15,5 @@ func UnFqdn(name string) string {
if n != 0 && name[n-1] == '.' { if n != 0 && name[n-1] == '.' {
return name[:n-1] return name[:n-1]
} }
return name return name
} }
// UnFqdnDomainsSeq generates a sequence of "unFQDNed" domain names derived from a domain (FQDN or not) in descending order.
func UnFqdnDomainsSeq(fqdn string) iter.Seq[string] {
return func(yield func(string) bool) {
if fqdn == "" {
return
}
for _, index := range dns.Split(fqdn) {
if !yield(UnFqdn(fqdn[index:])) {
return
}
}
}
}
// DomainsSeq generates a sequence of domain names derived from a domain (FQDN or not) in descending order.
func DomainsSeq(fqdn string) iter.Seq[string] {
return func(yield func(string) bool) {
if fqdn == "" {
return
}
for _, index := range dns.Split(fqdn) {
if !yield(fqdn[index:]) {
return
}
}
}
}

View file

@ -4,9 +4,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"net" "net"
"os"
"slices"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -16,7 +13,13 @@ import (
const defaultResolvConf = "/etc/resolv.conf" const defaultResolvConf = "/etc/resolv.conf"
var fqdnSoaCache = &sync.Map{} // dnsTimeout is used to override the default DNS timeout of 10 seconds.
var dnsTimeout = 10 * time.Second
var (
fqdnSoaCache = map[string]*soaCacheEntry{}
muFqdnSoaCache sync.Mutex
)
var defaultNameservers = []string{ var defaultNameservers = []string{
"google-public-dns-a.google.com:53", "google-public-dns-a.google.com:53",
@ -48,11 +51,9 @@ func (cache *soaCacheEntry) isExpired() bool {
// ClearFqdnCache clears the cache of fqdn to zone mappings. Primarily used in testing. // ClearFqdnCache clears the cache of fqdn to zone mappings. Primarily used in testing.
func ClearFqdnCache() { func ClearFqdnCache() {
// TODO(ldez): use `fqdnSoaCache.Clear()` when updating to go1.23 muFqdnSoaCache.Lock()
fqdnSoaCache.Range(func(k, v any) bool { fqdnSoaCache = map[string]*soaCacheEntry{}
fqdnSoaCache.Delete(k) muFqdnSoaCache.Unlock()
return true
})
} }
func AddDNSTimeout(timeout time.Duration) ChallengeOption { func AddDNSTimeout(timeout time.Duration) ChallengeOption {
@ -81,7 +82,6 @@ func getNameservers(path string, defaults []string) []string {
func ParseNameservers(servers []string) []string { func ParseNameservers(servers []string) []string {
var resolvers []string var resolvers []string
for _, resolver := range servers { for _, resolver := range servers {
// ensure all servers have a port number // ensure all servers have a port number
if _, _, err := net.SplitHostPort(resolver); err != nil { if _, _, err := net.SplitHostPort(resolver); err != nil {
@ -90,7 +90,6 @@ func ParseNameservers(servers []string) []string {
resolvers = append(resolvers, resolver) resolvers = append(resolvers, resolver)
} }
} }
return resolvers return resolvers
} }
@ -100,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 {
@ -117,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
@ -132,9 +130,8 @@ 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
} }
@ -149,62 +146,60 @@ 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
} }
func lookupSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error) { func lookupSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error) {
muFqdnSoaCache.Lock()
defer muFqdnSoaCache.Unlock()
// Do we have it cached and is it still fresh? // Do we have it cached and is it still fresh?
entAny, ok := fqdnSoaCache.Load(fqdn) if ent := fqdnSoaCache[fqdn]; ent != nil && !ent.isExpired() {
if ok && entAny != nil {
ent, ok1 := entAny.(*soaCacheEntry)
if ok1 && !ent.isExpired() {
return ent, nil return ent, nil
} }
}
ent, err := fetchSoaByFqdn(fqdn, nameservers) ent, err := fetchSoaByFqdn(fqdn, nameservers)
if err != nil { if err != nil {
return nil, err return nil, err
} }
fqdnSoaCache.Store(fqdn, ent) fqdnSoaCache[fqdn] = ent
return ent, nil return ent, nil
} }
func fetchSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error) { func fetchSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error) {
var ( var err error
err error var in *dns.Msg
r *dns.Msg
)
for domain := range DomainsSeq(fqdn) { labelIndexes := dns.Split(fqdn)
r, err = dnsQuery(domain, dns.TypeSOA, nameservers, true) for _, index := range labelIndexes {
domain := fqdn[index:]
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
} }
@ -213,48 +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 err error
}
var (
r *dns.Msg
err error
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 {
@ -270,85 +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 {
tcp := &dns.Client{Net: "tcp", Timeout: dnsTimeout}
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
}
udp := &dns.Client{Net: "udp", Timeout: dnsTimeout} udp := &dns.Client{Net: "udp", Timeout: dnsTimeout}
r, _, err := udp.Exchange(m, ns) in, _, err := udp.Exchange(m, ns)
if r != nil && r.Truncated { if in != nil && in.Truncated {
tcp := &dns.Client{Net: "tcp", Timeout: dnsTimeout} tcp := &dns.Client{Net: "tcp", Timeout: dnsTimeout}
// If the TCP request succeeds, the "err" will reset to nil // If the TCP request succeeds, the err will reset to nil
r, _, err = tcp.Exchange(m, ns) in, _, err = tcp.Exchange(m, ns)
}
return in, err
}
func formatDNSError(msg *dns.Msg, err error) string {
var parts []string
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

@ -4,15 +4,10 @@ import (
"fmt" "fmt"
"net" "net"
"strings" "strings"
"time"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
// defaultNameserverPort used by authoritative NS.
// This is for tests only.
var defaultNameserverPort = "53"
// PreCheckFunc checks DNS propagation before notifying ACME that the DNS challenge is ready. // PreCheckFunc checks DNS propagation before notifying ACME that the DNS challenge is ready.
type PreCheckFunc func(fqdn, value string) (bool, error) type PreCheckFunc func(fqdn, value string) (bool, error)
@ -28,53 +23,23 @@ func WrapPreCheck(wrap WrapPreCheckFunc) ChallengeOption {
} }
} }
// DisableCompletePropagationRequirement obsolete.
//
// Deprecated: use DisableAuthoritativeNssPropagationRequirement instead.
func DisableCompletePropagationRequirement() ChallengeOption { func DisableCompletePropagationRequirement() ChallengeOption {
return DisableAuthoritativeNssPropagationRequirement()
}
func DisableAuthoritativeNssPropagationRequirement() ChallengeOption {
return func(chlg *Challenge) error { return func(chlg *Challenge) error {
chlg.preCheck.requireAuthoritativeNssPropagation = false chlg.preCheck.requireCompletePropagation = false
return nil return nil
} }
} }
func RecursiveNSsPropagationRequirement() ChallengeOption {
return func(chlg *Challenge) error {
chlg.preCheck.requireRecursiveNssPropagation = true
return nil
}
}
func PropagationWait(wait time.Duration, skipCheck bool) ChallengeOption {
return WrapPreCheck(func(domain, fqdn, value string, check PreCheckFunc) (bool, error) {
time.Sleep(wait)
if skipCheck {
return true, nil
}
return check(fqdn, value)
})
}
type preCheck struct { type preCheck struct {
// checks DNS propagation before notifying ACME that the DNS challenge is ready. // checks DNS propagation before notifying ACME that the DNS challenge is ready.
checkFunc WrapPreCheckFunc checkFunc WrapPreCheckFunc
// require the TXT record to be propagated to all authoritative name servers // require the TXT record to be propagated to all authoritative name servers
requireAuthoritativeNssPropagation bool requireCompletePropagation bool
// require the TXT record to be propagated to all recursive name servers
requireRecursiveNssPropagation bool
} }
func newPreCheck() preCheck { func newPreCheck() preCheck {
return preCheck{ return preCheck{
requireAuthoritativeNssPropagation: true, requireCompletePropagation: true,
} }
} }
@ -88,48 +53,32 @@ func (p preCheck) call(domain, fqdn, value string) (bool, error) {
// checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers. // checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers.
func (p preCheck) checkDNSPropagation(fqdn, value string) (bool, error) { func (p preCheck) checkDNSPropagation(fqdn, value string) (bool, error) {
// Initial attempt to resolve at the recursive NS (require to get CNAME) // Initial attempt to resolve at the recursive NS
r, err := dnsQuery(fqdn, dns.TypeTXT, recursiveNameservers, true) r, err := dnsQuery(fqdn, dns.TypeTXT, recursiveNameservers, true)
if err != nil { if err != nil {
return false, fmt.Errorf("initial recursive nameserver: %w", err) return false, err
}
if !p.requireCompletePropagation {
return true, nil
} }
if r.Rcode == dns.RcodeSuccess { if r.Rcode == dns.RcodeSuccess {
fqdn = updateDomainWithCName(r, fqdn) fqdn = updateDomainWithCName(r, fqdn)
} }
if p.requireRecursiveNssPropagation {
_, err = checkNameserversPropagation(fqdn, value, recursiveNameservers, false)
if err != nil {
return false, fmt.Errorf("recursive nameservers: %w", err)
}
}
if !p.requireAuthoritativeNssPropagation {
return true, nil
}
authoritativeNss, err := lookupNameservers(fqdn) authoritativeNss, err := lookupNameservers(fqdn)
if err != nil { if err != nil {
return false, err return false, err
} }
found, err := checkNameserversPropagation(fqdn, value, authoritativeNss, true) return checkAuthoritativeNss(fqdn, value, authoritativeNss)
if err != nil {
return found, fmt.Errorf("authoritative nameservers: %w", err)
}
return found, nil
} }
// checkNameserversPropagation queries each of the given nameservers for the expected TXT record. // checkAuthoritativeNss queries each of the given nameservers for the expected TXT record.
func checkNameserversPropagation(fqdn, value string, nameservers []string, addPort bool) (bool, error) { func checkAuthoritativeNss(fqdn, value string, nameservers []string) (bool, error) {
for _, ns := range nameservers { for _, ns := range nameservers {
if addPort { r, err := dnsQuery(fqdn, dns.TypeTXT, []string{net.JoinHostPort(ns, "53")}, false)
ns = net.JoinHostPort(ns, defaultNameserverPort)
}
r, err := dnsQuery(fqdn, dns.TypeTXT, []string{ns}, false)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -141,11 +90,9 @@ func checkNameserversPropagation(fqdn, value string, nameservers []string, addPo
var records []string var records []string
var found bool var found bool
for _, rr := range r.Answer { for _, rr := range r.Answer {
if txt, ok := rr.(*dns.TXT); ok { if txt, ok := rr.(*dns.TXT); ok {
record := strings.Join(txt.Txt, "") record := strings.Join(txt.Txt, "")
records = append(records, record) records = append(records, record)
if record == value { if record == value {
found = true found = true

View file

@ -3,7 +3,6 @@ package http01
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"net/netip"
"strings" "strings"
) )
@ -24,16 +23,13 @@ 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!)
@ -55,10 +51,10 @@ func (m *hostMatcher) name() string {
} }
func (m *hostMatcher) matches(r *http.Request, domain string) bool { func (m *hostMatcher) matches(r *http.Request, domain string) bool {
return matchDomain(r.Host, domain) return strings.HasPrefix(r.Host, domain)
} }
// arbitraryMatcher checks whether the specified (*net/http.Request).Header value starts with a domain name. // hostMatcher checks whether the specified (*net/http.Request).Header value starts with a domain name.
type arbitraryMatcher string type arbitraryMatcher string
func (m arbitraryMatcher) name() string { func (m arbitraryMatcher) name() string {
@ -66,11 +62,11 @@ func (m arbitraryMatcher) name() string {
} }
func (m arbitraryMatcher) matches(r *http.Request, domain string) bool { func (m arbitraryMatcher) matches(r *http.Request, domain string) bool {
return matchDomain(r.Header.Get(m.name()), domain) return strings.HasPrefix(r.Header.Get(m.name()), domain)
} }
// 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 {
@ -88,8 +84,7 @@ func (m *forwardedMatcher) matches(r *http.Request, domain string) bool {
} }
host := fwds[0]["host"] host := fwds[0]["host"]
return strings.HasPrefix(host, domain)
return matchDomain(host, domain)
} }
// parsing requires some form of state machine. // parsing requires some form of state machine.
@ -100,7 +95,6 @@ func parseForwardedHeader(s string) (elements []map[string]string, err error) {
inquote := false inquote := false
pos := 0 pos := 0
l := len(s) l := len(s)
for i := 0; i < l; i++ { for i := 0; i < l; i++ {
r := rune(s[i]) r := rune(s[i])
@ -112,7 +106,6 @@ func parseForwardedHeader(s string) (elements []map[string]string, err error) {
pos = i pos = i
inquote = false inquote = false
} }
continue continue
} }
@ -121,7 +114,6 @@ func parseForwardedHeader(s string) (elements []map[string]string, err error) {
if key == "" { if key == "" {
return nil, fmt.Errorf("unexpected quoted string as pos %d", i) return nil, fmt.Errorf("unexpected quoted string as pos %d", i)
} }
inquote = true inquote = true
pos = i + 1 pos = i + 1
@ -138,10 +130,11 @@ func parseForwardedHeader(s string) (elements []map[string]string, err error) {
case r == ',': // end of forwarded-element case r == ',': // end of forwarded-element
if key != "" { if key != "" {
if val == "" {
val = s[pos:i] val = s[pos:i]
}
cur[key] = val cur[key] = val
} }
elements = append(elements, cur) elements = append(elements, cur)
cur = make(map[string]string) cur = make(map[string]string)
key = "" key = ""
@ -164,14 +157,11 @@ func parseForwardedHeader(s string) (elements []map[string]string, err error) {
if pos < len(s) { if pos < len(s) {
val = s[pos:] val = s[pos:]
} }
cur[key] = val cur[key] = val
} }
if len(cur) > 0 { if len(cur) > 0 {
elements = append(elements, cur) elements = append(elements, cur)
} }
return elements, nil return elements, nil
} }
@ -186,19 +176,9 @@ func skipWS(s string, i int) int {
for isWS(rune(s[i+1])) { for isWS(rune(s[i+1])) {
i++ i++
} }
return i return i
} }
func isWS(r rune) bool { func isWS(r rune) bool {
return strings.ContainsRune(" \t\v\r\n", r) return strings.ContainsRune(" \t\v\r\n", r)
} }
func matchDomain(src, domain string) bool {
addr, err := netip.ParseAddr(domain)
if err == nil && addr.Is6() {
domain = "[" + domain + "]"
}
return strings.HasPrefix(src, domain)
}

View file

@ -2,7 +2,6 @@ package http01
import ( import (
"fmt" "fmt"
"time"
"github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme"
"github.com/go-acme/lego/v4/acme/api" "github.com/go-acme/lego/v4/acme/api"
@ -12,16 +11,6 @@ import (
type ValidateFunc func(core *api.Core, domain string, chlng acme.Challenge) error type ValidateFunc func(core *api.Core, domain string, chlng acme.Challenge) error
type ChallengeOption func(*Challenge) error
// SetDelay sets a delay between the start of the HTTP server and the challenge validation.
func SetDelay(delay time.Duration) ChallengeOption {
return func(chlg *Challenge) error {
chlg.delay = delay
return nil
}
}
// ChallengePath returns the URL path for the `http-01` challenge. // ChallengePath returns the URL path for the `http-01` challenge.
func ChallengePath(token string) string { func ChallengePath(token string) string {
return "/.well-known/acme-challenge/" + token return "/.well-known/acme-challenge/" + token
@ -31,24 +20,14 @@ type Challenge struct {
core *api.Core core *api.Core
validate ValidateFunc validate ValidateFunc
provider challenge.Provider provider challenge.Provider
delay time.Duration
} }
func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider, opts ...ChallengeOption) *Challenge { func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider) *Challenge {
chlg := &Challenge{ return &Challenge{
core: core, core: core,
validate: validate, validate: validate,
provider: provider, provider: provider,
} }
for _, opt := range opts {
err := opt(chlg)
if err != nil {
log.Infof("challenge option error: %v", err)
}
}
return chlg
} }
func (c *Challenge) SetProvider(provider challenge.Provider) { func (c *Challenge) SetProvider(provider challenge.Provider) {
@ -74,7 +53,6 @@ func (c *Challenge) Solve(authz acme.Authorization) error {
if err != nil { if err != nil {
return fmt.Errorf("[%s] acme: error presenting token: %w", domain, err) return fmt.Errorf("[%s] acme: error presenting token: %w", domain, err)
} }
defer func() { defer func() {
err := c.provider.CleanUp(authz.Identifier.Value, chlng.Token, keyAuth) err := c.provider.CleanUp(authz.Identifier.Value, chlng.Token, keyAuth)
if err != nil { if err != nil {
@ -82,11 +60,6 @@ func (c *Challenge) Solve(authz acme.Authorization) error {
} }
}() }()
if c.delay > 0 {
time.Sleep(c.delay)
}
chlng.KeyAuthorization = keyAuth chlng.KeyAuthorization = keyAuth
return c.validate(c.core, domain, chlng) return c.validate(c.core, domain, chlng)
} }

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,37 +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("tcp", s.GetAddress())
s.listener, err = net.Listen(s.network, 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)`.
@ -72,11 +54,8 @@ func (s *ProviderServer) CleanUp(domain, token, keyAuth string) error {
if s.listener == nil { if s.listener == nil {
return nil return nil
} }
s.listener.Close() s.listener.Close()
<-s.done <-s.done
return nil return nil
} }
@ -90,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 {
@ -106,32 +85,27 @@ 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()
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet && s.matcher.matches(r, domain) { if r.Method == http.MethodGet && s.matcher.matches(r, domain) {
w.Header().Set("Content-Type", "text/plain") w.Header().Set("Content-Type", "text/plain")
_, err := w.Write([]byte(keyAuth)) _, err := w.Write([]byte(keyAuth))
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
log.Infof("[%s] Served key authentication", domain) log.Infof("[%s] Served key authentication", domain)
} else {
return 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())
}
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())
_, 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)
return return
} }
}
}) })
httpServer := &http.Server{Handler: mux} httpServer := &http.Server{Handler: mux}
@ -144,6 +118,5 @@ func (s *ProviderServer) serve(domain, token, keyAuth string) {
if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
log.Println(err) log.Println(err)
} }
s.done <- true s.done <- true
} }

View file

@ -16,12 +16,10 @@ func (e obtainError) Error() string {
for domain := range e { for domain := range e {
domains = append(domains, domain) domains = append(domains, domain)
} }
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

@ -50,14 +50,11 @@ func NewProber(solverManager *SolverManager) *Prober {
func (p *Prober) Solve(authorizations []acme.Authorization) error { func (p *Prober) Solve(authorizations []acme.Authorization) error {
failures := make(obtainError) failures := make(obtainError)
var ( var authSolvers []*selectedAuthSolver
authSolvers []*selectedAuthSolver var authSolversSequential []*selectedAuthSolver
authSolversSequential []*selectedAuthSolver
)
// Loop through the resources, basically through the domains. // Loop through the resources, basically through the domains.
// First pass just selects a solver for each authz. // First pass just selects a solver for each authz.
for _, authz := range authorizations { for _, authz := range authorizations {
domain := challenge.GetTargetedDomain(authz) domain := challenge.GetTargetedDomain(authz)
if authz.Status == acme.StatusValid { if authz.Status == acme.StatusValid {
@ -93,7 +90,6 @@ func (p *Prober) Solve(authorizations []acme.Authorization) error {
if len(failures) > 0 { if len(failures) > 0 {
return failures return failures
} }
return nil return nil
} }
@ -106,9 +102,7 @@ func sequentialSolve(authSolvers []*selectedAuthSolver, failures obtainError) {
err := solvr.PreSolve(authSolver.authz) err := solvr.PreSolve(authSolver.authz)
if err != nil { if err != nil {
failures[domain] = err failures[domain] = err
cleanUp(authSolver.solver, authSolver.authz) cleanUp(authSolver.solver, authSolver.authz)
continue continue
} }
} }
@ -117,9 +111,7 @@ func sequentialSolve(authSolvers []*selectedAuthSolver, failures obtainError) {
err := authSolver.solver.Solve(authSolver.authz) err := authSolver.solver.Solve(authSolver.authz)
if err != nil { if err != nil {
failures[domain] = err failures[domain] = err
cleanUp(authSolver.solver, authSolver.authz) cleanUp(authSolver.solver, authSolver.authz)
continue continue
} }
@ -136,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 {
@ -157,7 +149,6 @@ func parallelSolve(authSolvers []*selectedAuthSolver, failures obtainError) {
// Finally solve all challenges for real // Finally solve all challenges for real
for _, authSolver := range authSolvers { for _, authSolver := range authSolvers {
authz := authSolver.authz authz := authSolver.authz
domain := challenge.GetTargetedDomain(authz) domain := challenge.GetTargetedDomain(authz)
if failures[domain] != nil { if failures[domain] != nil {
// already failed in previous loop // already failed in previous loop
@ -174,7 +165,6 @@ func parallelSolve(authSolvers []*selectedAuthSolver, failures obtainError) {
func cleanUp(solvr solver, authz acme.Authorization) { func cleanUp(solvr solver, authz acme.Authorization) {
if solvr, ok := solvr.(cleanup); ok { if solvr, ok := solvr.(cleanup); ok {
domain := challenge.GetTargetedDomain(authz) domain := challenge.GetTargetedDomain(authz)
err := solvr.CleanUp(authz) err := solvr.CleanUp(authz)
if err != nil { if err != nil {
log.Warnf("[%s] acme: cleaning up failed: %v ", domain, err) log.Warnf("[%s] acme: cleaning up failed: %v ", domain, err)

View file

@ -8,7 +8,7 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/cenkalti/backoff/v5" "github.com/cenkalti/backoff/v4"
"github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme"
"github.com/go-acme/lego/v4/acme/api" "github.com/go-acme/lego/v4/acme/api"
"github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge"
@ -16,7 +16,6 @@ import (
"github.com/go-acme/lego/v4/challenge/http01" "github.com/go-acme/lego/v4/challenge/http01"
"github.com/go-acme/lego/v4/challenge/tlsalpn01" "github.com/go-acme/lego/v4/challenge/tlsalpn01"
"github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/log"
"github.com/go-acme/lego/v4/platform/wait"
) )
type byType []acme.Challenge type byType []acme.Challenge
@ -38,14 +37,14 @@ func NewSolversManager(core *api.Core) *SolverManager {
} }
// SetHTTP01Provider specifies a custom provider p that can solve the given HTTP-01 challenge. // SetHTTP01Provider specifies a custom provider p that can solve the given HTTP-01 challenge.
func (c *SolverManager) SetHTTP01Provider(p challenge.Provider, opts ...http01.ChallengeOption) error { func (c *SolverManager) SetHTTP01Provider(p challenge.Provider) error {
c.solvers[challenge.HTTP01] = http01.NewChallenge(c.core, validate, p, opts...) c.solvers[challenge.HTTP01] = http01.NewChallenge(c.core, validate, p)
return nil return nil
} }
// SetTLSALPN01Provider specifies a custom provider p that can solve the given TLS-ALPN-01 challenge. // SetTLSALPN01Provider specifies a custom provider p that can solve the given TLS-ALPN-01 challenge.
func (c *SolverManager) SetTLSALPN01Provider(p challenge.Provider, opts ...tlsalpn01.ChallengeOption) error { func (c *SolverManager) SetTLSALPN01Provider(p challenge.Provider) error {
c.solvers[challenge.TLSALPN01] = tlsalpn01.NewChallenge(c.core, validate, p, opts...) c.solvers[challenge.TLSALPN01] = tlsalpn01.NewChallenge(c.core, validate, p)
return nil return nil
} }
@ -55,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)
} }
@ -71,7 +70,6 @@ func (c *SolverManager) chooseSolver(authz acme.Authorization) solver {
log.Infof("[%s] acme: use %s solver", domain, chlg.Type) log.Infof("[%s] acme: use %s solver", domain, chlg.Type)
return solvr return solvr
} }
log.Infof("[%s] acme: Could not find solver for: %s", domain, chlg.Type) log.Infof("[%s] acme: Could not find solver for: %s", domain, chlg.Type)
} }
@ -102,26 +100,28 @@ func validate(core *api.Core, domain string, chlg acme.Challenge) error {
// https://github.com/letsencrypt/boulder/blob/master/docs/acme-divergences.md#section-82 // https://github.com/letsencrypt/boulder/blob/master/docs/acme-divergences.md#section-82
ra = 5 ra = 5
} }
initialInterval := time.Duration(ra) * time.Second initialInterval := time.Duration(ra) * time.Second
ctx := context.Background()
bo := backoff.NewExponentialBackOff() bo := backoff.NewExponentialBackOff()
bo.InitialInterval = initialInterval bo.InitialInterval = initialInterval
bo.MaxInterval = 10 * initialInterval bo.MaxInterval = 10 * 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 {
@ -129,12 +129,10 @@ func validate(core *api.Core, domain string, chlg acme.Challenge) error {
return nil return nil
} }
return fmt.Errorf("the server didn't respond to our request (status=%s)", authz.Status) return errors.New("the server didn't respond to our request")
} }
return wait.Retry(ctx, operation, return backoff.Retry(operation, backoff.WithContext(bo, ctx))
backoff.WithBackOff(bo),
backoff.WithMaxElapsedTime(100*initialInterval))
} }
func checkChallengeStatus(chlng acme.ExtendedChallenge) (bool, error) { func checkChallengeStatus(chlng acme.ExtendedChallenge) (bool, error) {
@ -144,9 +142,9 @@ func checkChallengeStatus(chlng acme.ExtendedChallenge) (bool, error) {
case acme.StatusPending, acme.StatusProcessing: case acme.StatusPending, acme.StatusProcessing:
return false, nil return false, nil
case acme.StatusInvalid: case acme.StatusInvalid:
return false, fmt.Errorf("invalid challenge: %w", chlng.Err()) return false, chlng.Error
default: default:
return false, fmt.Errorf("the server returned an unexpected challenge status: %s", chlng.Status) return false, errors.New("the server returned an unexpected state")
} }
} }
@ -161,12 +159,11 @@ func checkAuthorizationStatus(authz acme.Authorization) (bool, error) {
case acme.StatusInvalid: case acme.StatusInvalid:
for _, chlg := range authz.Challenges { for _, chlg := range authz.Challenges {
if chlg.Status == acme.StatusInvalid && chlg.Error != nil { if chlg.Status == acme.StatusInvalid && chlg.Error != nil {
return false, fmt.Errorf("invalid authorization: %w", chlg.Err()) return false, chlg.Error
} }
} }
return false, fmt.Errorf("the authorization state %s", authz.Status)
return false, errors.New("invalid authorization")
default: default:
return false, fmt.Errorf("the server returned an unexpected authorization status: %s", authz.Status) return false, errors.New("the server returned an unexpected state")
} }
} }

View file

@ -7,7 +7,6 @@ import (
"crypto/x509/pkix" "crypto/x509/pkix"
"encoding/asn1" "encoding/asn1"
"fmt" "fmt"
"time"
"github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme"
"github.com/go-acme/lego/v4/acme/api" "github.com/go-acme/lego/v4/acme/api"
@ -17,43 +16,23 @@ 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
type ChallengeOption func(*Challenge) error
// SetDelay sets a delay between the start of the TLS listener and the challenge validation.
func SetDelay(delay time.Duration) ChallengeOption {
return func(chlg *Challenge) error {
chlg.delay = delay
return nil
}
}
type Challenge struct { type Challenge struct {
core *api.Core core *api.Core
validate ValidateFunc validate ValidateFunc
provider challenge.Provider provider challenge.Provider
delay time.Duration
} }
func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider, opts ...ChallengeOption) *Challenge { func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider) *Challenge {
chlg := &Challenge{ return &Challenge{
core: core, core: core,
validate: validate, validate: validate,
provider: provider, provider: provider,
} }
for _, opt := range opts {
err := opt(chlg)
if err != nil {
log.Infof("challenge option error: %v", err)
}
}
return chlg
} }
func (c *Challenge) SetProvider(provider challenge.Provider) { func (c *Challenge) SetProvider(provider challenge.Provider) {
@ -80,7 +59,6 @@ func (c *Challenge) Solve(authz acme.Authorization) error {
if err != nil { if err != nil {
return fmt.Errorf("[%s] acme: error presenting token: %w", challenge.GetTargetedDomain(authz), err) return fmt.Errorf("[%s] acme: error presenting token: %w", challenge.GetTargetedDomain(authz), err)
} }
defer func() { defer func() {
err := c.provider.CleanUp(domain, chlng.Token, keyAuth) err := c.provider.CleanUp(domain, chlng.Token, keyAuth)
if err != nil { if err != nil {
@ -88,12 +66,7 @@ func (c *Challenge) Solve(authz acme.Authorization) error {
} }
}() }()
if c.delay > 0 {
time.Sleep(c.delay)
}
chlng.KeyAuthorization = keyAuth chlng.KeyAuthorization = keyAuth
return c.validate(c.core, domain, chlng) return c.validate(c.core, domain, chlng)
} }
@ -110,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

@ -53,15 +53,7 @@ func NewClient(config *Config) (*Client, error) {
solversManager := resolver.NewSolversManager(core) solversManager := resolver.NewSolversManager(core)
prober := resolver.NewProber(solversManager) prober := resolver.NewProber(solversManager)
certifier := certificate.NewCertifier(core, prober, certificate.CertifierOptions{KeyType: config.Certificate.KeyType, Timeout: config.Certificate.Timeout})
options := certificate.CertifierOptions{
KeyType: config.Certificate.KeyType,
Timeout: config.Certificate.Timeout,
OverallRequestLimit: config.Certificate.OverallRequestLimit,
DisableCommonName: config.Certificate.DisableCommonName,
}
certifier := certificate.NewCertifier(core, prober, options)
return &Client{ return &Client{
Certificate: certifier, Certificate: certifier,

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"
@ -63,8 +57,6 @@ func NewConfig(user registration.User) *Config {
type CertificateConfig struct { type CertificateConfig struct {
KeyType certcrypto.KeyType KeyType certcrypto.KeyType
Timeout time.Duration Timeout time.Duration
OverallRequestLimit int
DisableCommonName bool
} }
// createDefaultHTTPClient Creates an HTTP client with a reasonable timeout value // createDefaultHTTPClient Creates an HTTP client with a reasonable timeout value
@ -90,60 +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
}
useSystemCertPool, _ := strconv.ParseBool(os.Getenv(caSystemCertPool))
caCerts := strings.Split(customCACertsPath, string(os.PathListSeparator))
certPool, err := CreateCertPool(caCerts, useSystemCertPool)
if err != nil { if err != nil {
panic(fmt.Sprintf("create certificates pool: %v", err)) panic(fmt.Sprintf("error reading %s=%q: %v",
caCertificatesEnvVar, customCACertsPath, err))
} }
certPool := x509.NewCertPool()
return certPool
}
// CreateCertPool creates a *x509.CertPool populated with the PEM certificates.
func CreateCertPool(caCerts []string, useSystemCertPool bool) (*x509.CertPool, error) {
if len(caCerts) == 0 {
return nil, nil
}
certPool := newCertPool(useSystemCertPool)
for _, customPath := range caCerts {
customCAs, err := os.ReadFile(customPath)
if err != nil {
return nil, fmt.Errorf("error reading %q: %w", customPath, err)
}
if ok := certPool.AppendCertsFromPEM(customCAs); !ok { if ok := certPool.AppendCertsFromPEM(customCAs); !ok {
return nil, fmt.Errorf("error creating x509 cert pool from %q: %w", customPath, err) panic(fmt.Sprintf("error creating x509 cert pool from %s=%q: %v",
caCertificatesEnvVar, customCACertsPath, err))
} }
return certPool
} }
return nil
return certPool, nil
}
func newCertPool(useSystemCertPool bool) *x509.CertPool {
if !useSystemCertPool {
return x509.NewCertPool()
}
pool, err := x509.SystemCertPool()
if err == nil {
return pool
}
return x509.NewCertPool()
} }

View file

@ -10,50 +10,50 @@ var Logger StdLogger = log.New(os.Stderr, "", log.LstdFlags)
// StdLogger interface for Standard Logger. // StdLogger interface for Standard Logger.
type StdLogger interface { type StdLogger interface {
Fatal(args ...any) Fatal(args ...interface{})
Fatalln(args ...any) Fatalln(args ...interface{})
Fatalf(format string, args ...any) Fatalf(format string, args ...interface{})
Print(args ...any) Print(args ...interface{})
Println(args ...any) Println(args ...interface{})
Printf(format string, args ...any) Printf(format string, args ...interface{})
} }
// Fatal writes a log entry. // Fatal writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger. // It uses Logger if not nil, otherwise it uses the default log.Logger.
func Fatal(args ...any) { func Fatal(args ...interface{}) {
Logger.Fatal(args...) Logger.Fatal(args...)
} }
// Fatalf writes a log entry. // Fatalf writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger. // It uses Logger if not nil, otherwise it uses the default log.Logger.
func Fatalf(format string, args ...any) { func Fatalf(format string, args ...interface{}) {
Logger.Fatalf(format, args...) Logger.Fatalf(format, args...)
} }
// Print writes a log entry. // Print writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger. // It uses Logger if not nil, otherwise it uses the default log.Logger.
func Print(args ...any) { func Print(args ...interface{}) {
Logger.Print(args...) Logger.Print(args...)
} }
// Println writes a log entry. // Println writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger. // It uses Logger if not nil, otherwise it uses the default log.Logger.
func Println(args ...any) { func Println(args ...interface{}) {
Logger.Println(args...) Logger.Println(args...)
} }
// Printf writes a log entry. // Printf writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger. // It uses Logger if not nil, otherwise it uses the default log.Logger.
func Printf(format string, args ...any) { func Printf(format string, args ...interface{}) {
Logger.Printf(format, args...) Logger.Printf(format, args...)
} }
// Warnf writes a log entry. // Warnf writes a log entry.
func Warnf(format string, args ...any) { func Warnf(format string, args ...interface{}) {
Printf("[WARN] "+format, args...) Printf("[WARN] "+format, args...)
} }
// Infof writes a log entry. // Infof writes a log entry.
func Infof(format string, args ...any) { func Infof(format string, args ...interface{}) {
Printf("[INFO] "+format, args...) Printf("[INFO] "+format, args...)
} }

View file

@ -3,6 +3,7 @@ package env
import ( import (
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"strconv" "strconv"
"strings" "strings"
@ -16,13 +17,11 @@ func Get(names ...string) (map[string]string, error) {
values := map[string]string{} values := map[string]string{}
var missingEnvVars []string var missingEnvVars []string
for _, envVar := range names { for _, envVar := range names {
value := GetOrFile(envVar) value := GetOrFile(envVar)
if value == "" { if value == "" {
missingEnvVars = append(missingEnvVars, envVar) missingEnvVars = append(missingEnvVars, envVar)
} }
values[envVar] = value values[envVar] = value
} }
@ -56,11 +55,11 @@ func Get(names ...string) (map[string]string, error) {
// // 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{}
var missingEnvVars []string var missingEnvVars []string
for _, names := range groups { for _, names := range groups {
if len(names) == 0 { if len(names) == 0 {
return nil, errors.New("undefined environment variable names") return nil, errors.New("undefined environment variable names")
@ -71,7 +70,6 @@ func GetWithFallback(groups ...[]string) (map[string]string, error) {
missingEnvVars = append(missingEnvVars, envVar) missingEnvVars = append(missingEnvVars, envVar)
continue continue
} }
values[envVar] = value values[envVar] = value
} }
@ -82,26 +80,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
} }
} }
@ -109,32 +96,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, defaultValue string) string { func GetOrDefaultString(envVar, defaultValue string) string {
return getOrDefault(envVar, defaultValue, ParseString) v := GetOrFile(envVar)
if v == "" {
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
} }
@ -152,13 +150,12 @@ func GetOrFile(envVar string) string {
} }
fileVar := envVar + "_FILE" fileVar := envVar + "_FILE"
fileVarValue := os.Getenv(fileVar) fileVarValue := os.Getenv(fileVar)
if fileVarValue == "" { if fileVarValue == "" {
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 ""
@ -166,43 +163,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
}
// ParsePairs parses a raw string of comma-separated key-value pairs into a map.
// Keys and values are separated by a colon and are trimmed of whitespace.
func ParsePairs(raw string) (map[string]string, error) {
result := make(map[string]string)
for pair := range strings.SplitSeq(strings.TrimSuffix(raw, ","), ",") {
data := strings.Split(pair, ":")
if len(data) != 2 {
return nil, fmt.Errorf("incorrect pair: %s", pair)
}
result[strings.TrimSpace(data[0])] = strings.TrimSpace(data[1])
}
return result, nil
}

View file

@ -1,11 +1,10 @@
package wait package wait
import ( import (
"context" "errors"
"fmt" "fmt"
"time" "time"
"github.com/cenkalti/backoff/v5"
"github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/log"
) )
@ -14,25 +13,21 @@ func For(msg string, timeout, interval time.Duration, f func() (bool, error)) er
log.Infof("Wait for %s [timeout: %s, interval: %s]", msg, timeout, interval) log.Infof("Wait for %s [timeout: %s, interval: %s]", msg, timeout, interval)
var lastErr error var lastErr error
timeUp := time.After(timeout) timeUp := time.After(timeout)
for { for {
select { select {
case <-timeUp: case <-timeUp:
if lastErr == nil { if lastErr == nil {
return fmt.Errorf("%s: time limit exceeded", msg) return errors.New("time limit exceeded")
} }
return fmt.Errorf("time limit exceeded: last error: %w", lastErr)
return fmt.Errorf("%s: time limit exceeded: last error: %w", msg, lastErr)
default: default:
} }
stop, err := f() stop, err := f()
if stop { if stop {
return err return nil
} }
if err != nil { if err != nil {
lastErr = err lastErr = err
} }
@ -40,13 +35,3 @@ func For(msg string, timeout, interval time.Duration, f func() (bool, error)) er
time.Sleep(interval) time.Sleep(interval)
} }
} }
// Retry retries the given operation until it succeeds or the context is canceled.
// Similar to [backoff.Retry] but with a different signature.
func Retry(ctx context.Context, operation func() error, opts ...backoff.RetryOption) error {
_, err := backoff.Retry(ctx, func() (any, error) {
return nil, operation()
}, opts...)
return err
}

View file

@ -1 +0,0 @@
/testdata/** text eol=lf

View file

@ -1,134 +0,0 @@
package clientdebug
import (
"fmt"
"io"
"net/http"
"net/http/httputil"
"os"
"regexp"
"strconv"
"strings"
"github.com/go-acme/lego/v4/platform/config/env"
)
const replacement = "***"
type Option func(*DumpTransport)
func WithEnvKeys(keys ...string) Option {
return func(d *DumpTransport) {
for _, key := range keys {
v := strings.TrimSpace(env.GetOrFile(key))
if v == "" {
continue
}
d.replacements = append(d.replacements, v, replacement)
}
}
}
func WithValues(values ...string) Option {
return func(d *DumpTransport) {
for _, value := range values {
d.replacements = append(d.replacements, value, replacement)
}
}
}
func WithHeaders(keys ...string) Option {
return func(d *DumpTransport) {
d.regexps = append(d.regexps,
regexp.MustCompile(fmt.Sprintf(`(?im)^(%s):.+$`, strings.Join(keys, "|"))))
}
}
type DumpTransport struct {
rt http.RoundTripper
replacements []string
replacer *strings.Replacer
regexps []*regexp.Regexp
writer io.Writer
}
func NewDumpTransport(rt http.RoundTripper, opts ...Option) *DumpTransport {
if rt == nil {
rt = http.DefaultTransport
}
d := &DumpTransport{
rt: rt,
writer: os.Stdout,
}
for _, opt := range opts {
opt(d)
}
d.regexps = append(d.regexps,
regexp.MustCompile(`(?im)^(Authorization):.+$`),
regexp.MustCompile(`(?im)^(Token|X-Token):.+$`),
regexp.MustCompile(`(?im)^(Auth-Token|X-Auth-Token):.+$`),
regexp.MustCompile(`(?im)^(Api-Key|X-Api-Key|X-Api-Secret):.+$`),
)
if len(d.replacements) > 0 {
d.replacer = strings.NewReplacer(d.replacements...)
}
return d
}
func (d *DumpTransport) RoundTrip(h *http.Request) (*http.Response, error) {
data, _ := httputil.DumpRequestOut(h, true)
_, _ = fmt.Fprintln(d.writer, "[HTTP Request]")
_, _ = fmt.Fprintln(d.writer, d.redact(data))
resp, err := d.rt.RoundTrip(h)
if err != nil {
return nil, err
}
data, _ = httputil.DumpResponse(resp, true)
_, _ = fmt.Fprintln(d.writer, "[HTTP Response]")
_, _ = fmt.Fprintln(d.writer, d.redact(data))
return resp, err
}
func (d *DumpTransport) redact(content []byte) string {
data := string(content)
for _, r := range d.regexps {
data = r.ReplaceAllString(data, "$1: "+replacement)
}
if d.replacer == nil {
return data
}
return d.replacer.Replace(data)
}
// Wrap wraps an HTTP client Transport with the [DumpTransport].
func Wrap(client *http.Client, opts ...Option) *http.Client {
val, found := os.LookupEnv("LEGO_DEBUG_DNS_API_HTTP_CLIENT")
if !found {
return client
}
if ok, _ := strconv.ParseBool(val); !ok {
return client
}
client.Transport = NewDumpTransport(client.Transport, opts...)
return client
}

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

@ -1,29 +0,0 @@
// Code generated by 'internal/releaser'; DO NOT EDIT.
package useragent
import (
"fmt"
"net/http"
"runtime"
)
const (
// ourUserAgent is the User-Agent of this underlying library package.
ourUserAgent = "goacme-lego/4.29.0"
// ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package.
// values: detach|release
// NOTE: Update this with each tagged release.
ourUserAgentComment = "release"
)
// Get builds and returns the User-Agent string.
func Get() string {
return fmt.Sprintf("%s (%s; %s; %s)", ourUserAgent, ourUserAgentComment, runtime.GOOS, runtime.GOARCH)
}
// SetHeader sets the User-Agent header.
func SetHeader(h http.Header) {
h.Set("User-Agent", Get())
}

View file

@ -5,26 +5,26 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"strings"
"sync" "sync"
"time" "time"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/platform/config/env"
"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
"github.com/go-acme/lego/v4/providers/dns/internal/useragent"
"github.com/ovh/go-ovh/ovh" "github.com/ovh/go-ovh/ovh"
) )
// 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/?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"
@ -32,24 +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"
)
// EnvAccessToken Authenticate using Access Token client.
const EnvAccessToken = envNamespace + "ACCESS_TOKEN"
var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
// Record a DNS record. // Record a DNS record.
type Record struct { type Record struct {
ID int64 `json:"id,omitempty"` ID int64 `json:"id,omitempty"`
@ -60,24 +42,12 @@ 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
AccessToken string
PropagationTimeout time.Duration PropagationTimeout time.Duration
PollingInterval time.Duration PollingInterval time.Duration
TTL int TTL int
@ -96,10 +66,6 @@ func NewDefaultConfig() *Config {
} }
} }
func (c *Config) hasAppKeyAuth() bool {
return c.ApplicationKey != "" || c.ApplicationSecret != "" || c.ConsumerKey != ""
}
// DNSProvider implements the challenge.Provider interface. // DNSProvider implements the challenge.Provider interface.
type DNSProvider struct { type DNSProvider struct {
config *Config config *Config
@ -112,26 +78,16 @@ 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) {
values, err := env.Get(EnvEndpoint, EnvApplicationKey, EnvApplicationSecret, EnvConsumerKey)
if err != nil {
return nil, fmt.Errorf("ovh: %w", err)
}
config := NewDefaultConfig() config := NewDefaultConfig()
config.APIEndpoint = values[EnvEndpoint]
// https://github.com/ovh/go-ovh/blob/6817886d12a8c5650794b28da635af9fcdfd1162/ovh/configuration.go#L105 config.ApplicationKey = values[EnvApplicationKey]
config.APIEndpoint = env.GetOrDefaultString(EnvEndpoint, "ovh-eu") config.ApplicationSecret = values[EnvApplicationSecret]
config.ConsumerKey = values[EnvConsumerKey]
config.ApplicationKey = env.GetOrFile(EnvApplicationKey)
config.ApplicationSecret = env.GetOrFile(EnvApplicationSecret)
config.ConsumerKey = env.GetOrFile(EnvConsumerKey)
config.AccessToken = env.GetOrFile(EnvAccessToken)
clientID := env.GetOrFile(EnvClientID)
clientSecret := env.GetOrFile(EnvClientSecret)
if clientID != "" || clientSecret != "" {
config.OAuth2Config = &OAuth2Config{
ClientID: clientID,
ClientSecret: clientSecret,
}
}
return NewDNSProviderConfig(config) return NewDNSProviderConfig(config)
} }
@ -142,27 +98,22 @@ 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() && config.AccessToken != "" { if config.APIEndpoint == "" || config.ApplicationKey == "" || config.ApplicationSecret == "" || config.ConsumerKey == "" {
return nil, errors.New("ovh: can't use multiple authentication systems (ApplicationKey, OAuth2, Access Token)") return nil, errors.New("ovh: credentials missing")
} }
if config.OAuth2Config != nil && config.AccessToken != "" { client, err := ovh.NewClient(
return nil, errors.New("ovh: can't use multiple authentication systems (OAuth2, Access Token)") config.APIEndpoint,
} config.ApplicationKey,
config.ApplicationSecret,
if config.OAuth2Config != nil && config.hasAppKeyAuth() { config.ConsumerKey,
return nil, errors.New("ovh: can't use multiple authentication systems (ApplicationKey, OAuth2)") )
}
if config.hasAppKeyAuth() && config.AccessToken != "" {
return nil, errors.New("ovh: can't use multiple authentication systems (ApplicationKey, Access Token)")
}
client, err := newClient(config)
if err != nil { if err != nil {
return nil, fmt.Errorf("ovh: %w", err) return nil, fmt.Errorf("ovh: %w", err)
} }
client.Client = config.HTTPClient
return &DNSProvider{ return &DNSProvider{
config: config, config: config,
client: client, client: client,
@ -172,26 +123,22 @@ 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)
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) // Parse domain name
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
err = d.client.Post(reqURL, reqData, &respData) err = d.client.Post(reqURL, reqData, &respData)
if err != nil { if err != nil {
return fmt.Errorf("ovh: error when call api to add record (%s): %w", reqURL, err) return fmt.Errorf("ovh: error when call api to add record (%s): %w", reqURL, err)
@ -199,7 +146,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
// Apply the change // Apply the change
reqURL = fmt.Sprintf("/domain/zone/%s/refresh", authZone) reqURL = fmt.Sprintf("/domain/zone/%s/refresh", authZone)
err = d.client.Post(reqURL, nil, nil) err = d.client.Post(reqURL, nil, nil)
if err != nil { if err != nil {
return fmt.Errorf("ovh: error when call api to refresh zone (%s): %w", reqURL, err) return fmt.Errorf("ovh: error when call api to refresh zone (%s): %w", reqURL, err)
@ -214,20 +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)
@ -241,7 +186,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
// Apply the change // Apply the change
reqURL = fmt.Sprintf("/domain/zone/%s/refresh", authZone) reqURL = fmt.Sprintf("/domain/zone/%s/refresh", authZone)
err = d.client.Post(reqURL, nil, nil) err = d.client.Post(reqURL, nil, nil)
if err != nil { if err != nil {
return fmt.Errorf("ovh: error when call api to refresh zone (%s): %w", reqURL, err) return fmt.Errorf("ovh: error when call api to refresh zone (%s): %w", reqURL, err)
@ -261,34 +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 newClient(config *Config) (*ovh.Client, error) { func extractRecordName(fqdn, zone string) string {
var ( name := dns01.UnFqdn(fqdn)
client *ovh.Client if idx := strings.Index(name, "."+zone); idx != -1 {
err error return name[:idx]
)
switch {
case config.hasAppKeyAuth():
client, err = ovh.NewClient(config.APIEndpoint, config.ApplicationKey, config.ApplicationSecret, config.ConsumerKey)
case config.OAuth2Config != nil:
client, err = ovh.NewOAuth2Client(config.APIEndpoint, config.OAuth2Config.ClientID, config.OAuth2Config.ClientSecret)
case config.AccessToken != "":
client, err = ovh.NewAccessTokenClient(config.APIEndpoint, config.AccessToken)
default:
client, err = ovh.NewDefaultClient()
} }
return name
if err != nil {
return nil, fmt.Errorf("new client: %w", err)
}
client.UserAgent = useragent.Get()
if config.HTTPClient != nil {
client.Client = config.HTTPClient
}
client.Client = clientdebug.Wrap(client.Client)
return client, nil
} }

View file

@ -5,26 +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 -d '*.example.com' -d example.com run lego --email myemail@example.com --dns ovh --domains my.example.org run
# Or Access Token:
OVH_ACCESS_TOKEN=xxx \
OVH_ENDPOINT=ovh-eu \
lego --email you@example.com --dns ovh -d '*.example.com' -d example.com run
# Or OAuth2:
OVH_CLIENT_ID=yyy \
OVH_CLIENT_SECRET=xxx \
OVH_ENDPOINT=ovh-eu \
lego --email you@example.com --dns ovh -d '*.example.com' -d example.com run
''' '''
Additional = ''' Additional = '''
@ -32,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
{ {
@ -48,38 +33,19 @@ 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)"
OVH_ACCESS_TOKEN = "Access token"
[Configuration.Additional] [Configuration.Additional]
OVH_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" OVH_POLLING_INTERVAL = "Time between DNS propagation check"
OVH_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" OVH_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
OVH_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" OVH_TTL = "The TTL of the TXT record used for the DNS challenge"
OVH_HTTP_TIMEOUT = "API request timeout in seconds (Default: 180)" OVH_HTTP_TIMEOUT = "API request timeout"
[Links] [Links]
API = "https://eu.api.ovh.com/" API = "https://eu.api.ovh.com/"

View file

@ -1,236 +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"
)
// APIKeyHeader API key header.
const APIKeyHeader = "X-Api-Key"
// 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(APIKeyHeader, 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,257 +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"
"strconv"
"time"
"github.com/go-acme/lego/v4/challenge"
"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/internal/clientdebug"
"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"
)
var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
// 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.HTTPClient != nil {
client.HTTPClient = config.HTTPClient
}
client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
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 {
ctx := context.Background()
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)
}
zone, err := d.client.GetHostedZone(ctx, authZone)
if err != nil {
return fmt.Errorf("pdns: get hosted zone for %s: %w", authZone, 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)
var records []internal.Record
if existingRRSet != nil {
records = existingRRSet.Records
}
records = append(records, internal.Record{
Content: strconv.Quote(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: records,
}},
}
err = d.client.UpdateRecords(ctx, zone, rrSets)
if err != nil {
return fmt.Errorf("pdns: update records: %w", err)
}
err = d.client.Notify(ctx, zone)
if err != nil {
return fmt.Errorf("pdns: notify: %w", err)
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
ctx := context.Background()
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)
}
zone, err := d.client.GetHostedZone(ctx, authZone)
if err != nil {
return fmt.Errorf("pdns: get hosted zone for %s: %w", authZone, err)
}
// Look for existing records.
set := findTxtRecord(zone, info.EffectiveFQDN)
if set == nil {
return fmt.Errorf("pdns: no existing record found for %s", info.EffectiveFQDN)
}
var records []internal.Record
for _, r := range set.Records {
if r.Content != strconv.Quote(info.Value) {
records = append(records, r)
}
}
rrSet := internal.RRSet{
Name: set.Name,
Type: set.Type,
}
if len(records) > 0 {
rrSet.ChangeType = "REPLACE"
rrSet.TTL = d.config.TTL
rrSet.Records = records
} else {
rrSet.ChangeType = "DELETE"
}
err = d.client.UpdateRecords(ctx, zone, internal.RRSets{RRSets: []internal.RRSet{rrSet}})
if err != nil {
return fmt.Errorf("pdns: update records: %w", err)
}
err = d.client.Notify(ctx, zone)
if err != nil {
return fmt.Errorf("pdns: notify: %w", err)
}
return nil
}
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 -d '*.example.com' -d example.com 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 in seconds (Default: 2)"
PDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)"
PDNS_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)"
PDNS_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)"
[Links]
API = "https://doc.powerdns.com/md/httpapi/README/"

View file

@ -9,13 +9,11 @@ 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"` Body acme.Account `json:"body,omitempty"`
URI string `json:"uri,omitempty"` URI string `json:"uri,omitempty"`
} }
@ -54,13 +52,13 @@ 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 // seems impossible
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,13 +76,13 @@ 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 // seems impossible
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
@ -160,7 +158,6 @@ func (r *Registrar) ResolveAccountByKey() (*Resource, error) {
log.Infof("acme: Trying to resolve account by key") log.Infof("acme: Trying to resolve account by key")
accMsg := acme.Account{OnlyReturnExisting: true} accMsg := acme.Account{OnlyReturnExisting: true}
account, err := r.core.Accounts.New(accMsg) account, err := r.core.Accounts.New(accMsg)
if err != nil { if err != nil {
return nil, err return nil, 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,108 +0,0 @@
# Go JOSE
[![godoc](https://pkg.go.dev/badge/github.com/go-jose/go-jose/v4.svg)](https://pkg.go.dev/github.com/go-jose/go-jose/v4)
[![godoc](https://pkg.go.dev/badge/github.com/go-jose/go-jose/v4/jwt.svg)](https://pkg.go.dev/github.com/go-jose/go-jose/v4/jwt)
[![license](https://img.shields.io/badge/license-apache_2.0-blue.svg?style=flat)](https://raw.githubusercontent.com/go-jose/go-jose/master/LICENSE)
Package jose aims to provide an implementation of the Javascript Object Signing
and Encryption set of standards. This includes support for JSON Web Encryption,
JSON Web Signature, and JSON Web Token standards.
## Overview
The implementation follows the
[JSON Web Encryption](https://dx.doi.org/10.17487/RFC7516) (RFC 7516),
[JSON Web Signature](https://dx.doi.org/10.17487/RFC7515) (RFC 7515), and
[JSON Web Token](https://dx.doi.org/10.17487/RFC7519) (RFC 7519) specifications.
Tables of supported algorithms are shown below. The library supports both
the compact and JWS/JWE JSON Serialization formats, and has optional support for
multiple recipients. It also comes with a small command-line utility
([`jose-util`](https://pkg.go.dev/github.com/go-jose/go-jose/jose-util))
for dealing with JOSE messages in a shell.
**Note**: We use a forked version of the `encoding/json` package from the Go
standard library which uses case-sensitive matching for member names (instead
of [case-insensitive matching](https://www.ietf.org/mail-archive/web/json/current/msg03763.html)).
This is to avoid differences in interpretation of messages between go-jose and
libraries in other languages.
### Versions
The forthcoming Version 5 will be released with several breaking API changes,
and will require Golang's `encoding/json/v2`, which is currently requires
Go 1.25 built with GOEXPERIMENT=jsonv2.
Version 4 is the current stable version:
import "github.com/go-jose/go-jose/v4"
It supports at least the current and previous Golang release. Currently it
requires Golang 1.24.
Version 3 is only receiving critical security updates. Migration to Version 4 is recommended.
Versions 1 and 2 are obsolete, but can be found in the old repository, [square/go-jose](https://github.com/square/go-jose).
### Supported algorithms
See below for a table of supported algorithms. Algorithm identifiers match
the names in the [JSON Web Algorithms](https://dx.doi.org/10.17487/RFC7518)
standard where possible. The Godoc reference has a list of constants.
| Key encryption | Algorithm identifier(s) |
|:-----------------------|:-----------------------------------------------|
| RSA-PKCS#1v1.5 | RSA1_5 |
| RSA-OAEP | RSA-OAEP, RSA-OAEP-256 |
| AES key wrap | A128KW, A192KW, A256KW |
| AES-GCM key wrap | A128GCMKW, A192GCMKW, A256GCMKW |
| ECDH-ES + AES key wrap | ECDH-ES+A128KW, ECDH-ES+A192KW, ECDH-ES+A256KW |
| ECDH-ES (direct) | ECDH-ES<sup>1</sup> |
| Direct encryption | dir<sup>1</sup> |
<sup>1. Not supported in multi-recipient mode</sup>
| Signing / MAC | Algorithm identifier(s) |
|:------------------|:------------------------|
| RSASSA-PKCS#1v1.5 | RS256, RS384, RS512 |
| RSASSA-PSS | PS256, PS384, PS512 |
| HMAC | HS256, HS384, HS512 |
| ECDSA | ES256, ES384, ES512 |
| Ed25519 | EdDSA<sup>2</sup> |
<sup>2. Only available in version 2 of the package</sup>
| Content encryption | Algorithm identifier(s) |
|:-------------------|:--------------------------------------------|
| AES-CBC+HMAC | A128CBC-HS256, A192CBC-HS384, A256CBC-HS512 |
| AES-GCM | A128GCM, A192GCM, A256GCM |
| Compression | Algorithm identifiers(s) |
|:-------------------|--------------------------|
| DEFLATE (RFC 1951) | DEF |
### Supported key types
See below for a table of supported key types. These are understood by the
library, and can be passed to corresponding functions such as `NewEncrypter` or
`NewSigner`. Each of these keys can also be wrapped in a JWK if desired, which
allows attaching a key id.
| Algorithm(s) | Corresponding types |
|:------------------|--------------------------------------------------------------------------------------------------------------------------------------|
| RSA | *[rsa.PublicKey](https://pkg.go.dev/crypto/rsa/#PublicKey), *[rsa.PrivateKey](https://pkg.go.dev/crypto/rsa/#PrivateKey) |
| ECDH, ECDSA | *[ecdsa.PublicKey](https://pkg.go.dev/crypto/ecdsa/#PublicKey), *[ecdsa.PrivateKey](https://pkg.go.dev/crypto/ecdsa/#PrivateKey) |
| EdDSA<sup>1</sup> | [ed25519.PublicKey](https://pkg.go.dev/crypto/ed25519#PublicKey), [ed25519.PrivateKey](https://pkg.go.dev/crypto/ed25519#PrivateKey) |
| AES, HMAC | []byte |
<sup>1. Only available in version 2 or later of the package</sup>
## Examples
[![godoc](https://pkg.go.dev/badge/github.com/go-jose/go-jose/v4.svg)](https://pkg.go.dev/github.com/go-jose/go-jose/v4)
[![godoc](https://pkg.go.dev/badge/github.com/go-jose/go-jose/v4/jwt.svg)](https://pkg.go.dev/github.com/go-jose/go-jose/v4/jwt)
Examples can be found in the Godoc
reference for this package. The
[`jose-util`](https://github.com/go-jose/go-jose/tree/main/jose-util)
subdirectory also contains a small command-line utility which might be useful
as an example as well.

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

@ -48,17 +48,6 @@ linters:
- nlreturn - nlreturn
- testpackage - testpackage
- wsl - wsl
- varnamelen
- nilnil
- ireturn
- govet
- forcetypeassert
- cyclop
- containedctx
- revive
- nosnakecase
- exhaustruct
- depguard
issues: issues:
exclude-rules: exclude-rules:

View file

@ -1,168 +1,3 @@
# 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 # v0.7.8 - 2021/09/01
* Fix mapassign_faststr for indirect struct type ( #283 ) * Fix mapassign_faststr for indirect struct type ( #283 )

View file

@ -22,7 +22,7 @@ cover-html: cover
.PHONY: lint .PHONY: lint
lint: golangci-lint lint: golangci-lint
$(BIN_DIR)/golangci-lint run golangci-lint run
golangci-lint: | $(BIN_DIR) golangci-lint: | $(BIN_DIR)
@{ \ @{ \
@ -30,7 +30,7 @@ golangci-lint: | $(BIN_DIR)
GOLANGCI_LINT_TMP_DIR=$$(mktemp -d); \ GOLANGCI_LINT_TMP_DIR=$$(mktemp -d); \
cd $$GOLANGCI_LINT_TMP_DIR; \ cd $$GOLANGCI_LINT_TMP_DIR; \
go mod init tmp; \ go mod init tmp; \
GOBIN=$(BIN_DIR) go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.54.2; \ GOBIN=$(BIN_DIR) go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.36.0; \
rm -rf $$GOLANGCI_LINT_TMP_DIR; \ rm -rf $$GOLANGCI_LINT_TMP_DIR; \
} }

View file

@ -13,7 +13,7 @@ Fast JSON encoder/decoder compatible with encoding/json for Go
``` ```
* version ( expected release date ) * version ( expected release date )
* v0.9.0 * v0.7.0
| |
| while maintaining compatibility with encoding/json, we will add convenient APIs | while maintaining compatibility with encoding/json, we will add convenient APIs
| |
@ -21,8 +21,9 @@ Fast JSON encoder/decoder compatible with encoding/json for Go
* v1.0.0 * v1.0.0
``` ```
We are accepting requests for features that will be implemented between v0.9.0 and v.1.0.0. We are accepting requests for features that will be implemented between v0.7.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). If you have the API you need, please submit your issue [here](https://github.com/goccy/go-json/issues).
For example, I'm thinking of supporting `context.Context` of `json.Marshaler` and decoding using JSON Path.
# Features # Features
@ -31,7 +32,6 @@ If you have the API you need, please submit your issue [here](https://github.com
- Flexible customization with options - Flexible customization with options
- Coloring the encoded string - Coloring the encoded string
- Can propagate context.Context to `MarshalJSON` or `UnmarshalJSON` - Can propagate context.Context to `MarshalJSON` or `UnmarshalJSON`
- Can dynamically filter the fields of the structure type-safely
# Installation # Installation
@ -184,7 +184,7 @@ func Marshal(v interface{}) ([]byte, error) {
`json.Marshal` and `json.Unmarshal` receive `interface{}` value and they perform type determination dynamically to process. `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. 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. Therefore, the arguments for `Marshal` and `Unmarshal` are always escape to the heap.
However, `go-json` can use the feature of `reflect.Type` while avoiding escaping. 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. `reflect.Type` is defined as `interface`, but in reality `reflect.Type` is implemented only by the structure `rtype` defined in the `reflect` package.

View file

@ -83,37 +83,6 @@ func unmarshalContext(ctx context.Context, data []byte, v interface{}, optFuncs
return validateEndBuf(src, cursor) return validateEndBuf(src, cursor)
} }
var (
pathDecoder = decoder.NewPathDecoder()
)
func extractFromPath(path *Path, data []byte, optFuncs ...DecodeOptionFunc) ([][]byte, error) {
if path.path.RootSelectorOnly {
return [][]byte{data}, nil
}
src := make([]byte, len(data)+1) // append nul byte to the end
copy(src, data)
ctx := decoder.TakeRuntimeContext()
ctx.Buf = src
ctx.Option.Flags = 0
ctx.Option.Flags |= decoder.PathOption
ctx.Option.Path = path.path
for _, optFunc := range optFuncs {
optFunc(ctx.Option)
}
paths, cursor, err := pathDecoder.DecodePath(ctx, 0, 0)
if err != nil {
decoder.ReleaseRuntimeContext(ctx)
return nil, err
}
decoder.ReleaseRuntimeContext(ctx)
if err := validateEndBuf(src, cursor); err != nil {
return nil, err
}
return paths, nil
}
func unmarshalNoEscape(data []byte, v interface{}, optFuncs ...DecodeOptionFunc) error { func unmarshalNoEscape(data []byte, v interface{}, optFuncs ...DecodeOptionFunc) error {
src := make([]byte, len(data)+1) // append nul byte to the end src := make([]byte, len(data)+1) // append nul byte to the end
copy(src, data) copy(src, data)

View file

@ -1,7 +1,7 @@
version: '2' version: '2'
services: services:
go-json: go-json:
image: golang:1.18 image: golang:1.16
volumes: volumes:
- '.:/go/src/go-json' - '.:/go/src/go-json'
deploy: deploy:

View file

@ -3,7 +3,6 @@ package json
import ( import (
"context" "context"
"io" "io"
"os"
"unsafe" "unsafe"
"github.com/goccy/go-json/internal/encoder" "github.com/goccy/go-json/internal/encoder"
@ -52,7 +51,7 @@ func (e *Encoder) EncodeContext(ctx context.Context, v interface{}, optFuncs ...
rctx.Option.Flag |= encoder.ContextOption rctx.Option.Flag |= encoder.ContextOption
rctx.Option.Context = ctx rctx.Option.Context = ctx
err := e.encodeWithOption(rctx, v, optFuncs...) //nolint: contextcheck err := e.encodeWithOption(rctx, v, optFuncs...)
encoder.ReleaseRuntimeContext(rctx) encoder.ReleaseRuntimeContext(rctx)
return err return err
@ -62,8 +61,6 @@ func (e *Encoder) encodeWithOption(ctx *encoder.RuntimeContext, v interface{}, o
if e.enabledHTMLEscape { if e.enabledHTMLEscape {
ctx.Option.Flag |= encoder.HTMLEscapeOption ctx.Option.Flag |= encoder.HTMLEscapeOption
} }
ctx.Option.Flag |= encoder.NormalizeUTF8Option
ctx.Option.DebugOut = os.Stdout
for _, optFunc := range optFuncs { for _, optFunc := range optFuncs {
optFunc(ctx.Option) optFunc(ctx.Option)
} }
@ -114,13 +111,13 @@ func (e *Encoder) SetIndent(prefix, indent string) {
func marshalContext(ctx context.Context, v interface{}, optFuncs ...EncodeOptionFunc) ([]byte, error) { func marshalContext(ctx context.Context, v interface{}, optFuncs ...EncodeOptionFunc) ([]byte, error) {
rctx := encoder.TakeRuntimeContext() rctx := encoder.TakeRuntimeContext()
rctx.Option.Flag = 0 rctx.Option.Flag = 0
rctx.Option.Flag = encoder.HTMLEscapeOption | encoder.NormalizeUTF8Option | encoder.ContextOption rctx.Option.Flag = encoder.HTMLEscapeOption | encoder.ContextOption
rctx.Option.Context = ctx rctx.Option.Context = ctx
for _, optFunc := range optFuncs { for _, optFunc := range optFuncs {
optFunc(rctx.Option) optFunc(rctx.Option)
} }
buf, err := encode(rctx, v) //nolint: contextcheck buf, err := encode(rctx, v)
if err != nil { if err != nil {
encoder.ReleaseRuntimeContext(rctx) encoder.ReleaseRuntimeContext(rctx)
return nil, err return nil, err
@ -142,7 +139,7 @@ func marshal(v interface{}, optFuncs ...EncodeOptionFunc) ([]byte, error) {
ctx := encoder.TakeRuntimeContext() ctx := encoder.TakeRuntimeContext()
ctx.Option.Flag = 0 ctx.Option.Flag = 0
ctx.Option.Flag |= (encoder.HTMLEscapeOption | encoder.NormalizeUTF8Option) ctx.Option.Flag |= encoder.HTMLEscapeOption
for _, optFunc := range optFuncs { for _, optFunc := range optFuncs {
optFunc(ctx.Option) optFunc(ctx.Option)
} }
@ -169,7 +166,7 @@ func marshalNoEscape(v interface{}) ([]byte, error) {
ctx := encoder.TakeRuntimeContext() ctx := encoder.TakeRuntimeContext()
ctx.Option.Flag = 0 ctx.Option.Flag = 0
ctx.Option.Flag |= (encoder.HTMLEscapeOption | encoder.NormalizeUTF8Option) ctx.Option.Flag |= encoder.HTMLEscapeOption
buf, err := encodeNoEscape(ctx, v) buf, err := encodeNoEscape(ctx, v)
if err != nil { if err != nil {
@ -193,7 +190,7 @@ func marshalIndent(v interface{}, prefix, indent string, optFuncs ...EncodeOptio
ctx := encoder.TakeRuntimeContext() ctx := encoder.TakeRuntimeContext()
ctx.Option.Flag = 0 ctx.Option.Flag = 0
ctx.Option.Flag |= (encoder.HTMLEscapeOption | encoder.NormalizeUTF8Option | encoder.IndentOption) ctx.Option.Flag |= (encoder.HTMLEscapeOption | encoder.IndentOption)
for _, optFunc := range optFuncs { for _, optFunc := range optFuncs {
optFunc(ctx.Option) optFunc(ctx.Option)
} }
@ -223,7 +220,7 @@ func encode(ctx *encoder.RuntimeContext, v interface{}) ([]byte, error) {
typ := header.typ typ := header.typ
typeptr := uintptr(unsafe.Pointer(typ)) typeptr := uintptr(unsafe.Pointer(typ))
codeSet, err := encoder.CompileToGetCodeSet(ctx, typeptr) codeSet, err := encoder.CompileToGetCodeSet(typeptr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -251,7 +248,7 @@ func encodeNoEscape(ctx *encoder.RuntimeContext, v interface{}) ([]byte, error)
typ := header.typ typ := header.typ
typeptr := uintptr(unsafe.Pointer(typ)) typeptr := uintptr(unsafe.Pointer(typ))
codeSet, err := encoder.CompileToGetCodeSet(ctx, typeptr) codeSet, err := encoder.CompileToGetCodeSet(typeptr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -278,7 +275,7 @@ func encodeIndent(ctx *encoder.RuntimeContext, v interface{}, prefix, indent str
typ := header.typ typ := header.typ
typeptr := uintptr(unsafe.Pointer(typ)) typeptr := uintptr(unsafe.Pointer(typ))
codeSet, err := encoder.CompileToGetCodeSet(ctx, typeptr) codeSet, err := encoder.CompileToGetCodeSet(typeptr)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -37,5 +37,3 @@ type UnmarshalTypeError = errors.UnmarshalTypeError
type UnsupportedTypeError = errors.UnsupportedTypeError type UnsupportedTypeError = errors.UnsupportedTypeError
type UnsupportedValueError = errors.UnsupportedValueError type UnsupportedValueError = errors.UnsupportedValueError
type PathError = errors.PathError

View file

@ -35,7 +35,3 @@ func (d *anonymousFieldDecoder) Decode(ctx *RuntimeContext, cursor, depth int64,
p = *(*unsafe.Pointer)(p) p = *(*unsafe.Pointer)(p)
return d.dec.Decode(ctx, cursor, depth, unsafe.Pointer(uintptr(p)+d.offset)) return d.dec.Decode(ctx, cursor, depth, unsafe.Pointer(uintptr(p)+d.offset))
} }
func (d *anonymousFieldDecoder) DecodePath(ctx *RuntimeContext, cursor, depth int64) ([][]byte, int64, error) {
return d.dec.DecodePath(ctx, cursor, depth)
}

View file

@ -1,7 +1,6 @@
package decoder package decoder
import ( import (
"fmt"
"unsafe" "unsafe"
"github.com/goccy/go-json/internal/errors" "github.com/goccy/go-json/internal/errors"
@ -19,9 +18,7 @@ type arrayDecoder struct {
} }
func newArrayDecoder(dec Decoder, elemType *runtime.Type, alen int, structName, fieldName string) *arrayDecoder { func newArrayDecoder(dec Decoder, elemType *runtime.Type, alen int, structName, fieldName string) *arrayDecoder {
// workaround to avoid checkptr errors. cannot use `*(*unsafe.Pointer)(unsafe_New(elemType))` directly. zeroValue := *(*unsafe.Pointer)(unsafe_New(elemType))
zeroValuePtr := unsafe_New(elemType)
zeroValue := **(**unsafe.Pointer)(unsafe.Pointer(&zeroValuePtr))
return &arrayDecoder{ return &arrayDecoder{
valueDecoder: dec, valueDecoder: dec,
elemType: elemType, elemType: elemType,
@ -170,7 +167,3 @@ func (d *arrayDecoder) Decode(ctx *RuntimeContext, cursor, depth int64, p unsafe
} }
} }
} }
func (d *arrayDecoder) DecodePath(ctx *RuntimeContext, cursor, depth int64) ([][]byte, int64, error) {
return nil, 0, fmt.Errorf("json: array decoder does not support decode path")
}

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