2023-06-26 11:56:21 +01:00
|
|
|
package http_acme
|
2023-06-21 23:09:33 +01:00
|
|
|
|
|
|
|
import (
|
2023-07-03 16:27:24 +01:00
|
|
|
"encoding/json"
|
2023-06-23 23:00:09 +01:00
|
|
|
"fmt"
|
2023-06-21 23:09:33 +01:00
|
|
|
"github.com/go-acme/lego/v4/challenge"
|
2023-06-23 23:00:09 +01:00
|
|
|
"net/http"
|
|
|
|
"strings"
|
2023-06-21 23:09:33 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
var _ challenge.Provider = &HttpAcmeProvider{}
|
|
|
|
|
2023-07-03 16:27:24 +01:00
|
|
|
// HttpAcmeProvider sends HTTP requests to an API updating the outputted
|
|
|
|
// `.wellknown/acme-challenges` data
|
2023-06-21 23:09:33 +01:00
|
|
|
type HttpAcmeProvider struct {
|
|
|
|
accessToken, refreshToken string
|
|
|
|
apiUrlPresent, apiUrlCleanUp string
|
2023-07-03 16:27:24 +01:00
|
|
|
apiUrlRefreshToken string
|
2023-06-23 23:00:09 +01:00
|
|
|
trip http.RoundTripper
|
2023-06-21 23:09:33 +01:00
|
|
|
}
|
|
|
|
|
2023-07-03 16:27:24 +01:00
|
|
|
// NewHttpAcmeProvider creates a new HttpAcmeProvider using http.DefaultTransport
|
|
|
|
// as the transport
|
|
|
|
func NewHttpAcmeProvider(accessToken, refreshToken, apiUrlPresent, apiUrlCleanUp, apiUrlRefreshToken string) *HttpAcmeProvider {
|
|
|
|
return &HttpAcmeProvider{accessToken, refreshToken, apiUrlPresent, apiUrlCleanUp, apiUrlRefreshToken, http.DefaultTransport}
|
2023-06-21 23:09:33 +01:00
|
|
|
}
|
|
|
|
|
2023-07-03 16:27:24 +01:00
|
|
|
// Present implements challenge.Provider and sends a put request to the specified
|
|
|
|
// path along with a bearer token to authenticate
|
2023-06-21 23:09:33 +01:00
|
|
|
func (h *HttpAcmeProvider) Present(domain, token, keyAuth string) error {
|
2023-06-23 23:00:09 +01:00
|
|
|
// round trip
|
2023-07-03 16:27:24 +01:00
|
|
|
trip, err := h.authCheckRequest(http.MethodPut, h.apiUrlPresent, domain, token, keyAuth)
|
2023-06-23 23:00:09 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if trip.StatusCode != http.StatusOK {
|
|
|
|
return fmt.Errorf("Trip response status code was not 200")
|
|
|
|
}
|
|
|
|
return nil
|
2023-06-21 23:09:33 +01:00
|
|
|
}
|
|
|
|
|
2023-07-03 16:27:24 +01:00
|
|
|
// CleanUp implements challenge.Provider and sends a delete request to the
|
|
|
|
// specified path along with a bearer token to authenticate
|
2023-06-21 23:09:33 +01:00
|
|
|
func (h *HttpAcmeProvider) CleanUp(domain, token, keyAuth string) error {
|
2023-06-23 23:00:09 +01:00
|
|
|
// round trip
|
2023-07-03 16:27:24 +01:00
|
|
|
trip, err := h.authCheckRequest(http.MethodDelete, h.apiUrlCleanUp, domain, token, keyAuth)
|
2023-06-23 23:00:09 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if trip.StatusCode != http.StatusOK {
|
|
|
|
return fmt.Errorf("Trip response status code was not 200")
|
|
|
|
}
|
|
|
|
return nil
|
2023-06-21 23:09:33 +01:00
|
|
|
}
|
2023-07-03 16:27:24 +01:00
|
|
|
|
|
|
|
// authCheckRequest call internalRequest and renews the access token if it is
|
|
|
|
// outdated and calls internalRequest again
|
|
|
|
func (h *HttpAcmeProvider) authCheckRequest(method, url, domain, token, keyAuth string) (*http.Response, error) {
|
|
|
|
// call internal request and check the status code
|
|
|
|
resp, err := h.internalRequest(method, url, domain, token, keyAuth)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
switch resp.StatusCode {
|
|
|
|
case http.StatusOK:
|
|
|
|
// just return
|
|
|
|
return resp, nil
|
|
|
|
case http.StatusForbidden:
|
|
|
|
// send request to get renewed access and refresh tokens
|
|
|
|
req, err := http.NewRequest(http.MethodPost, h.apiUrlRefreshToken, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("refresh token request failed: %w", err)
|
|
|
|
}
|
|
|
|
req.Header.Set("Authorization", "Bearer "+h.refreshToken)
|
|
|
|
|
|
|
|
// round trip and status check
|
|
|
|
trip, err := h.trip.RoundTrip(req)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("refresh token request failed: %w", err)
|
|
|
|
}
|
|
|
|
if trip.StatusCode != http.StatusOK {
|
|
|
|
return nil, fmt.Errorf("refresh token request failed: due to invalid status code, expected 200 got %d", trip.StatusCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
// parse tokens from response body
|
|
|
|
var tokens struct {
|
|
|
|
Access string `json:"access"`
|
|
|
|
Refresh string `json:"refresh"`
|
|
|
|
}
|
|
|
|
if json.NewDecoder(trip.Body).Decode(&tokens) != nil {
|
|
|
|
return nil, fmt.Errorf("refresh token parsing failed: %w", err)
|
|
|
|
}
|
|
|
|
h.accessToken = tokens.Access
|
|
|
|
h.refreshToken = tokens.Refresh
|
|
|
|
|
|
|
|
// call internal request again
|
|
|
|
resp, err = h.internalRequest(method, url, domain, token, keyAuth)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if resp.StatusCode == http.StatusOK {
|
|
|
|
// just return
|
|
|
|
return resp, nil
|
|
|
|
}
|
|
|
|
return nil, fmt.Errorf("invalid status code, expected 200 got %d", resp.StatusCode)
|
|
|
|
}
|
|
|
|
// first request had an invalid status code
|
|
|
|
return nil, fmt.Errorf("invalid status code, expected 200/403 got %d", resp.StatusCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
// internalRequest sends a request to the acme challenge hosting api
|
|
|
|
func (h *HttpAcmeProvider) internalRequest(method, url, domain, token, keyAuth string) (*http.Response, error) {
|
|
|
|
v := strings.NewReplacer("%domain%", domain, "%token%", token, "%content%", keyAuth).Replace(url)
|
|
|
|
req, err := http.NewRequest(method, v, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
req.Header.Set("Authorization", "Bearer "+h.accessToken)
|
|
|
|
return h.trip.RoundTrip(req)
|
|
|
|
}
|