1
2
3 package acme
4
5 import (
6 "crypto/tls"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "io"
11 "net"
12 "net/http"
13 "net/url"
14 "time"
15 )
16
17 const (
18
19 NewNonceEndpoint Endpoint = "newNonce"
20
21 NewAccountEndpoint Endpoint = "newAccount"
22
23 NewOrderEndpoint Endpoint = "newOrder"
24
25 RevokeCertEndpoint Endpoint = "revokeCert"
26
27 KeyChangeEndpoint Endpoint = "keyChange"
28 )
29
30 var (
31
32 ErrEmptyDirectory = errors.New("directoryURL must not be empty")
33
34 ErrInvalidDirectoryURL = errors.New("directoryURL is not a valid URL")
35
36
37 ErrInvalidDirectoryHTTPCode = errors.New("GET request to directoryURL did not result in HTTP Status 200")
38
39
40 ErrInvalidDirectoryJSON = errors.New("GET request to directoryURL returned invalid JSON")
41
42
43 ErrInvalidDirectoryMeta = errors.New(`server's directory resource had invalid or missing "meta" key`)
44
45
46
47 ErrInvalidTermsOfService = errors.New(`server's directory resource had invalid or missing "meta.termsOfService" key`)
48
49
50
51
52
53 RequiredEndpoints = []Endpoint{
54 NewNonceEndpoint, NewAccountEndpoint,
55 NewOrderEndpoint, RevokeCertEndpoint,
56 }
57 )
58
59
60
61
62
63
64
65 type Endpoint string
66
67
68
69
70 type ErrMissingEndpoint struct {
71 endpoint Endpoint
72 }
73
74
75 func (e ErrMissingEndpoint) Error() string {
76 return fmt.Sprintf(
77 "directoryURL JSON was missing required key for %q endpoint",
78 e.endpoint,
79 )
80 }
81
82
83
84
85 type ErrInvalidEndpointURL struct {
86 endpoint Endpoint
87 value string
88 }
89
90
91 func (e ErrInvalidEndpointURL) Error() string {
92 return fmt.Sprintf(
93 "directoryURL JSON had invalid URL value (%q) for %q endpoint",
94 e.value, e.endpoint)
95 }
96
97
98
99
100
101
102
103 type Directory struct {
104
105
106 TermsOfService string
107
108 endpointURLs map[Endpoint]string
109 }
110
111
112
113
114
115 func getRawDirectory(directoryURL string) ([]byte, error) {
116 if directoryURL == "" {
117 return nil, ErrEmptyDirectory
118 }
119
120 if _, err := url.Parse(directoryURL); err != nil {
121 return nil, ErrInvalidDirectoryURL
122 }
123
124 httpClient := &http.Client{
125 Transport: &http.Transport{
126 DialContext: (&net.Dialer{
127 Timeout: 10 * time.Second,
128 KeepAlive: 30 * time.Second,
129 }).DialContext,
130 TLSHandshakeTimeout: 5 * time.Second,
131 TLSClientConfig: &tls.Config{
132
133
134
135 InsecureSkipVerify: true,
136 },
137 MaxIdleConns: 1,
138 IdleConnTimeout: 15 * time.Second,
139 },
140 Timeout: 10 * time.Second,
141 }
142
143 resp, err := httpClient.Get(directoryURL)
144 if err != nil {
145 return nil, err
146 }
147 defer resp.Body.Close()
148
149 if resp.StatusCode != http.StatusOK {
150 return nil, ErrInvalidDirectoryHTTPCode
151 }
152
153 rawDirectory, err := io.ReadAll(resp.Body)
154 if err != nil {
155 return nil, err
156 }
157
158 return rawDirectory, nil
159 }
160
161
162
163 func termsOfService(rawDirectory map[string]interface{}) (string, error) {
164 var directoryMeta map[string]interface{}
165
166 if rawDirectoryMeta, ok := rawDirectory["meta"]; !ok {
167 return "", ErrInvalidDirectoryMeta
168 } else if directoryMetaMap, ok := rawDirectoryMeta.(map[string]interface{}); !ok {
169 return "", ErrInvalidDirectoryMeta
170 } else {
171 directoryMeta = directoryMetaMap
172 }
173
174 rawToSURL, ok := directoryMeta["termsOfService"]
175 if !ok {
176 return "", ErrInvalidTermsOfService
177 }
178
179 tosURL, ok := rawToSURL.(string)
180 if !ok {
181 return "", ErrInvalidTermsOfService
182 }
183 return tosURL, nil
184 }
185
186
187
188
189 func NewDirectory(directoryURL string) (*Directory, error) {
190
191 dirContents, err := getRawDirectory(directoryURL)
192 if err != nil {
193 return nil, err
194 }
195
196
197 var dirResource map[string]interface{}
198 err = json.Unmarshal(dirContents, &dirResource)
199 if err != nil {
200 return nil, ErrInvalidDirectoryJSON
201 }
202
203
204
205 serverURL := func(name Endpoint) (*url.URL, error) {
206 if rawURL, ok := dirResource[string(name)]; !ok {
207 return nil, ErrMissingEndpoint{endpoint: name}
208 } else if urlString, ok := rawURL.(string); !ok {
209 return nil, ErrInvalidEndpointURL{endpoint: name, value: urlString}
210 } else if url, err := url.Parse(urlString); err != nil {
211 return nil, ErrInvalidEndpointURL{endpoint: name, value: urlString}
212 } else {
213 return url, nil
214 }
215 }
216
217
218 directory := &Directory{
219 endpointURLs: make(map[Endpoint]string),
220 }
221
222
223 for _, endpointName := range RequiredEndpoints {
224 url, err := serverURL(endpointName)
225 if err != nil {
226 return nil, err
227 }
228 directory.endpointURLs[endpointName] = url.String()
229 }
230
231
232 tos, err := termsOfService(dirResource)
233 if err != nil {
234 return nil, err
235 }
236 directory.TermsOfService = tos
237 return directory, nil
238 }
239
240
241
242
243 func (d *Directory) EndpointURL(ep Endpoint) string {
244 if url, ok := d.endpointURLs[ep]; ok {
245 return url
246 }
247
248 return ""
249 }
250
View as plain text