orchid/http-acme/http-acme-provider.go
2024-06-04 18:26:07 +01:00

158 lines
5.0 KiB
Go

package http_acme
import (
"encoding/json"
"fmt"
"github.com/1f349/orchid/logger"
"github.com/go-acme/lego/v4/challenge"
"gopkg.in/yaml.v3"
"net/http"
"os"
"strings"
)
var _ challenge.Provider = &HttpAcmeProvider{}
// HttpAcmeProvider sends HTTP requests to an API updating the outputted
// `.wellknown/acme-challenges` data
type HttpAcmeProvider struct {
tokenFile string
accessToken, refreshToken string
apiUrlPresent, apiUrlCleanUp string
apiUrlRefreshToken string
trip http.RoundTripper
}
type AcmeLogin struct {
Access string `yaml:"access"`
Refresh string `yaml:"refresh"`
}
// NewHttpAcmeProvider creates a new HttpAcmeProvider using http.DefaultTransport
// as the transport
func NewHttpAcmeProvider(tokenFile, apiUrlPresent, apiUrlCleanUp, apiUrlRefreshToken string) (*HttpAcmeProvider, error) {
// acme login token
openTokens, err := os.Open(tokenFile)
if err != nil {
return nil, fmt.Errorf("failed to load acme tokens: %w", err)
}
var acmeLogins AcmeLogin
err = yaml.NewDecoder(openTokens).Decode(&acmeLogins)
if err != nil {
return nil, fmt.Errorf("failed to load acme tokens: %w", err)
}
return &HttpAcmeProvider{tokenFile, acmeLogins.Access, acmeLogins.Refresh, apiUrlPresent, apiUrlCleanUp, apiUrlRefreshToken, http.DefaultTransport}, nil
}
// Present implements challenge.Provider and sends a put request to the specified
// path along with a bearer token to authenticate
func (h *HttpAcmeProvider) Present(domain, token, keyAuth string) error {
// round trip
trip, err := h.authCheckRequest(http.MethodPut, h.apiUrlPresent, domain, token, keyAuth)
if err != nil {
return err
}
if trip.StatusCode != http.StatusAccepted {
return fmt.Errorf("trip response status code was not 202")
}
return nil
}
// CleanUp implements challenge.Provider and sends a delete request to the
// specified path along with a bearer token to authenticate
func (h *HttpAcmeProvider) CleanUp(domain, token, keyAuth string) error {
// round trip
trip, err := h.authCheckRequest(http.MethodDelete, h.apiUrlCleanUp, domain, token, keyAuth)
if err != nil {
return err
}
if trip.StatusCode != http.StatusAccepted {
return fmt.Errorf("trip response status code was not 202")
}
return nil
}
// 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.StatusAccepted:
// 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.StatusAccepted {
return nil, fmt.Errorf("refresh token request failed: due to invalid status code, expected 202 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
go h.saveLoginTokens()
// call internal request again
resp, err = h.internalRequest(method, url, domain, token, keyAuth)
if err != nil {
return nil, err
}
if resp.StatusCode == http.StatusAccepted {
// just return
return resp, nil
}
return nil, fmt.Errorf("invalid status code, expected 202 got %d", resp.StatusCode)
}
// first request had an invalid status code
return nil, fmt.Errorf("invalid status code, expected 202/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, err
}
req.Header.Set("Authorization", "Bearer "+h.accessToken)
return h.trip.RoundTrip(req)
}
func (h *HttpAcmeProvider) saveLoginTokens() {
// acme login token
openTokens, err := os.Create(h.tokenFile)
if err != nil {
logger.Logger.Info("[Orchid] Failed to open token file:", "err", err)
}
defer openTokens.Close()
err = yaml.NewEncoder(openTokens).Encode(AcmeLogin{Access: h.accessToken, Refresh: h.refreshToken})
if err != nil {
logger.Logger.Info("[Orchid] Failed to write tokens file:", "err", err)
}
}