-
Notifications
You must be signed in to change notification settings - Fork 0
/
http-acme-provider.go
157 lines (140 loc) · 4.94 KB
/
http-acme-provider.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
package http_acme
import (
"encoding/json"
"fmt"
"github.com/go-acme/lego/v4/challenge"
"gopkg.in/yaml.v3"
"log"
"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 {
log.Println("[Orchid] Failed to open token file:", err)
}
defer openTokens.Close()
err = yaml.NewEncoder(openTokens).Encode(AcmeLogin{Access: h.accessToken, Refresh: h.refreshToken})
if err != nil {
log.Println("[Orchid] Failed to write tokens file:", err)
}
}