acme.go 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. package acme
  2. import (
  3. "bytes"
  4. "crypto"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "io/ioutil"
  9. "net/http"
  10. "os"
  11. "regexp"
  12. "strings"
  13. "time"
  14. )
  15. const (
  16. // LetsEncryptProduction holds the production directory url
  17. LetsEncryptProduction = "https://acme-v02.api.letsencrypt.org/directory"
  18. // LetsEncryptStaging holds the staging directory url
  19. LetsEncryptStaging = "https://acme-staging-v02.api.letsencrypt.org/directory"
  20. // ZeroSSLProduction holds the ZeroSSL directory url
  21. ZeroSSLProduction = "https://acme.zerossl.com/v2/DV90"
  22. userAgentString = "eggsampler-acme/v3 Go-http-client/1.1"
  23. )
  24. // NewClient creates a new acme client given a valid directory url.
  25. func NewClient(directoryURL string, options ...OptionFunc) (Client, error) {
  26. // Set a default http timeout of 60 seconds, this can be overridden
  27. // via an OptionFunc eg: acme.NewClient(url, WithHTTPTimeout(10 * time.Second))
  28. httpClient := &http.Client{
  29. Timeout: 60 * time.Second,
  30. }
  31. acmeClient := Client{
  32. httpClient: httpClient,
  33. nonces: &nonceStack{},
  34. retryCount: 5,
  35. }
  36. acmeClient.dir.URL = directoryURL
  37. for _, opt := range options {
  38. if err := opt(&acmeClient); err != nil {
  39. return acmeClient, fmt.Errorf("acme: error setting option: %v", err)
  40. }
  41. }
  42. if _, err := acmeClient.get(directoryURL, &acmeClient.dir, http.StatusOK); err != nil {
  43. return acmeClient, err
  44. }
  45. return acmeClient, nil
  46. }
  47. // Directory is the object returned by the client connecting to a directory url.
  48. func (c Client) Directory() Directory {
  49. return c.dir
  50. }
  51. // Helper function to get the poll interval and poll timeout, defaulting if 0
  52. func (c Client) getPollingDurations() (time.Duration, time.Duration) {
  53. pollInterval := c.PollInterval
  54. if pollInterval == 0 {
  55. pollInterval = 500 * time.Millisecond
  56. }
  57. pollTimeout := c.PollTimeout
  58. if pollTimeout == 0 {
  59. pollTimeout = 30 * time.Second
  60. }
  61. return pollInterval, pollTimeout
  62. }
  63. // Helper function to have a central point for performing http requests. Stores
  64. // any returned nonces in the stack. The caller is responsible for closing the
  65. // body so they can read the response.
  66. func (c Client) do(req *http.Request, addNonce bool) (*http.Response, error) {
  67. // identifier for this client, as well as the default go user agent
  68. if c.userAgentSuffix != "" {
  69. req.Header.Set("User-Agent", userAgentString+" "+c.userAgentSuffix)
  70. } else {
  71. req.Header.Set("User-Agent", userAgentString)
  72. }
  73. if c.acceptLanguage != "" {
  74. req.Header.Set("Accept-Language", c.acceptLanguage)
  75. }
  76. resp, err := c.httpClient.Do(req)
  77. if err != nil {
  78. return resp, err
  79. }
  80. if addNonce {
  81. c.nonces.push(resp.Header.Get("Replay-Nonce"))
  82. }
  83. return resp, nil
  84. }
  85. // Helper function to perform an HTTP get request and read the body. The caller
  86. // is responsible for closing the body so they can read the response.
  87. func (c Client) getRaw(url string, expectedStatus ...int) (*http.Response, []byte, error) {
  88. req, err := http.NewRequest(http.MethodGet, url, nil)
  89. if err != nil {
  90. return nil, nil, fmt.Errorf("acme: error creating request: %v", err)
  91. }
  92. resp, err := c.do(req, true)
  93. if err != nil {
  94. return resp, nil, fmt.Errorf("acme: error fetching response: %v", err)
  95. }
  96. defer resp.Body.Close()
  97. if err := checkError(resp, expectedStatus...); err != nil {
  98. return resp, nil, err
  99. }
  100. body, err := ioutil.ReadAll(resp.Body)
  101. if err != nil {
  102. return resp, body, fmt.Errorf("acme: error reading response body: %v", err)
  103. }
  104. return resp, body, nil
  105. }
  106. // Helper function for performing a http get on an acme resource. The caller is
  107. // responsible for closing the body so they can read the response.
  108. func (c Client) get(url string, out interface{}, expectedStatus ...int) (*http.Response, error) {
  109. resp, body, err := c.getRaw(url, expectedStatus...)
  110. if err != nil {
  111. return resp, err
  112. }
  113. if len(body) > 0 && out != nil {
  114. if err := json.Unmarshal(body, out); err != nil {
  115. return resp, fmt.Errorf("acme: error parsing response body: %v", err)
  116. }
  117. }
  118. return resp, nil
  119. }
  120. func (c Client) nonce() (string, error) {
  121. nonce := c.nonces.pop()
  122. if nonce != "" {
  123. return nonce, nil
  124. }
  125. if c.dir.NewNonce == "" {
  126. return "", errors.New("acme: no new nonce url")
  127. }
  128. req, err := http.NewRequest("HEAD", c.dir.NewNonce, nil)
  129. if err != nil {
  130. return "", fmt.Errorf("acme: error creating new nonce request: %v", err)
  131. }
  132. resp, err := c.do(req, false)
  133. if err != nil {
  134. return "", fmt.Errorf("acme: error fetching new nonce: %v", err)
  135. }
  136. nonce = resp.Header.Get("Replay-Nonce")
  137. return nonce, nil
  138. }
  139. // Helper function to perform an HTTP post request and read the body. Will
  140. // attempt to retry if error is badNonce. The caller is responsible for closing
  141. // the body so they can read the response.
  142. func (c Client) postRaw(retryCount int, requestURL, kid string, privateKey crypto.Signer, payload interface{}, expectedStatus []int) (*http.Response, []byte, error) {
  143. nonce, err := c.nonce()
  144. if err != nil {
  145. return nil, nil, err
  146. }
  147. data, err := jwsEncodeJSON(payload, privateKey, KeyID(kid), nonce, requestURL)
  148. if err != nil {
  149. return nil, nil, fmt.Errorf("acme: error encoding json payload: %v", err)
  150. }
  151. req, err := http.NewRequest(http.MethodPost, requestURL, bytes.NewReader(data))
  152. if err != nil {
  153. return nil, nil, fmt.Errorf("acme: error creating request: %v", err)
  154. }
  155. req.Header.Set("Content-Type", "application/jose+json")
  156. resp, err := c.do(req, true)
  157. if err != nil {
  158. return resp, nil, fmt.Errorf("acme: error sending request: %v", err)
  159. }
  160. defer resp.Body.Close()
  161. if err := checkError(resp, expectedStatus...); err != nil {
  162. prob, ok := err.(Problem)
  163. if !ok {
  164. // don't retry for an error we don't know about
  165. return resp, nil, err
  166. }
  167. if retryCount >= c.retryCount {
  168. // don't attempt to retry if too many retries
  169. return resp, nil, err
  170. }
  171. if strings.HasSuffix(prob.Type, ":badNonce") {
  172. // only retry if error is badNonce
  173. return c.postRaw(retryCount+1, requestURL, kid, privateKey, payload, expectedStatus)
  174. }
  175. return resp, nil, err
  176. }
  177. body, err := ioutil.ReadAll(resp.Body)
  178. if err != nil {
  179. return resp, body, fmt.Errorf("acme: error reading response body: %v", err)
  180. }
  181. return resp, body, nil
  182. }
  183. // Helper function for performing a http post to an acme resource. The caller is
  184. // responsible for closing the body so they can read the response.
  185. func (c Client) post(requestURL, keyID string, privateKey crypto.Signer, payload interface{}, out interface{}, expectedStatus ...int) (*http.Response, error) {
  186. resp, body, err := c.postRaw(0, requestURL, keyID, privateKey, payload, expectedStatus)
  187. if err != nil {
  188. return resp, err
  189. }
  190. if _, b := os.LookupEnv("ACME_DEBUG_POST"); b {
  191. fmt.Println()
  192. fmt.Println("========= " + requestURL)
  193. fmt.Println(string(body))
  194. fmt.Println()
  195. }
  196. if len(body) > 0 && out != nil {
  197. if err := json.Unmarshal(body, out); err != nil {
  198. return resp, fmt.Errorf("acme: error parsing response: %v - %s", err, string(body))
  199. }
  200. }
  201. return resp, nil
  202. }
  203. var regLink = regexp.MustCompile(`<(.+?)>;\s*rel="(.+?)"`)
  204. // Fetches a http Link header from an http response and closes the body.
  205. func fetchLink(resp *http.Response, wantedLink string) string {
  206. if resp == nil {
  207. return ""
  208. }
  209. linkHeader := resp.Header["Link"]
  210. if len(linkHeader) == 0 {
  211. return ""
  212. }
  213. for _, l := range linkHeader {
  214. matches := regLink.FindAllStringSubmatch(l, -1)
  215. for _, m := range matches {
  216. if len(m) != 3 {
  217. continue
  218. }
  219. if m[2] == wantedLink {
  220. return m[1]
  221. }
  222. }
  223. }
  224. return ""
  225. }
  226. // Fetch is a helper function to assist with POST-AS-GET requests
  227. func (c Client) Fetch(account Account, requestURL string, result interface{}, expectedStatus ...int) error {
  228. if len(expectedStatus) == 0 {
  229. expectedStatus = []int{http.StatusOK}
  230. }
  231. _, err := c.post(requestURL, account.URL, account.PrivateKey, "", result, expectedStatus...)
  232. return err
  233. }
  234. // Fetches all http Link header from a http response
  235. func fetchLinks(resp *http.Response, wantedLink string) []string {
  236. if resp == nil {
  237. return nil
  238. }
  239. linkHeader := resp.Header["Link"]
  240. if len(linkHeader) == 0 {
  241. return nil
  242. }
  243. var links []string
  244. for _, l := range linkHeader {
  245. matches := regLink.FindAllStringSubmatch(l, -1)
  246. for _, m := range matches {
  247. if len(m) != 3 {
  248. continue
  249. }
  250. if m[2] == wantedLink {
  251. links = append(links, m[1])
  252. }
  253. }
  254. }
  255. return links
  256. }