-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: Fernandez Ludovic <[email protected]>
- Loading branch information
Showing
10 changed files
with
680 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
--- | ||
title: "Vercel" | ||
date: 2019-03-03T16:39:46+01:00 | ||
draft: false | ||
slug: vercel | ||
--- | ||
|
||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. --> | ||
<!-- providers/dns/vercel/vercel.toml --> | ||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. --> | ||
|
||
Since: v4.7.0 | ||
|
||
Configuration for [Vercel](https://vercel.com). | ||
|
||
|
||
<!--more--> | ||
|
||
- Code: `vercel` | ||
|
||
Here is an example bash command using the Vercel provider: | ||
|
||
```bash | ||
VERCEL_API_TOKEN=xxxxxx \ | ||
lego --email [email protected] --dns vercel --domains my.example.org run | ||
``` | ||
|
||
|
||
|
||
|
||
## Credentials | ||
|
||
| Environment Variable Name | Description | | ||
|-----------------------|-------------| | ||
| `VERCEL_API_TOKEN` | Authentication token | | ||
|
||
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. | ||
More information [here](/lego/dns/#configuration-and-credentials). | ||
|
||
|
||
## Additional Configuration | ||
|
||
| Environment Variable Name | Description | | ||
|--------------------------------|-------------| | ||
| `VERCEL_HTTP_TIMEOUT` | API request timeout | | ||
| `VERCEL_POLLING_INTERVAL` | Time between DNS propagation check | | ||
| `VERCEL_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | ||
| `VERCEL_TEAM_ID` | Team ID (ex: team_xxxxxxxxxxxxxxxxxxxxxxxx) | | ||
| `VERCEL_TTL` | The TTL of the TXT record used for the DNS challenge | | ||
|
||
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. | ||
More information [here](/lego/dns/#configuration-and-credentials). | ||
|
||
|
||
|
||
|
||
## More information | ||
|
||
- [API documentation](https://vercel.com/docs/rest-api#endpoints/dns) | ||
|
||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. --> | ||
<!-- providers/dns/vercel/vercel.toml --> | ||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. --> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
package internal | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"net/url" | ||
"path" | ||
"time" | ||
|
||
"github.com/go-acme/lego/v4/challenge/dns01" | ||
) | ||
|
||
const defaultBaseURL = "https://api.vercel.com" | ||
|
||
// Client Vercel client. | ||
type Client struct { | ||
authToken string | ||
teamID string | ||
baseURL *url.URL | ||
HTTPClient *http.Client | ||
} | ||
|
||
// NewClient creates a Client. | ||
func NewClient(authToken string, teamID string) *Client { | ||
baseURL, _ := url.Parse(defaultBaseURL) | ||
|
||
return &Client{ | ||
authToken: authToken, | ||
teamID: teamID, | ||
baseURL: baseURL, | ||
HTTPClient: &http.Client{Timeout: 10 * time.Second}, | ||
} | ||
} | ||
|
||
// CreateRecord creates a DNS record. | ||
// https://vercel.com/docs/rest-api#endpoints/dns/create-a-dns-record | ||
func (c *Client) CreateRecord(zone string, record Record) (*CreateRecordResponse, error) { | ||
endpoint, err := c.baseURL.Parse(path.Join(c.baseURL.Path, "v2", "domains", dns01.UnFqdn(zone), "records")) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
body, err := json.Marshal(record) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
req, err := c.newRequest(http.MethodPost, endpoint.String(), bytes.NewReader(body)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
resp, err := c.HTTPClient.Do(req) | ||
if err != nil { | ||
return nil, err | ||
} | ||
defer func() { _ = resp.Body.Close() }() | ||
|
||
if resp.StatusCode >= 400 { | ||
return nil, readError(req, resp) | ||
} | ||
|
||
content, err := io.ReadAll(resp.Body) | ||
if err != nil { | ||
return nil, errors.New(toUnreadableBodyMessage(req, content)) | ||
} | ||
|
||
// Everything looks good; but we'll need the ID later to delete the record | ||
respData := &CreateRecordResponse{} | ||
err = json.Unmarshal(content, respData) | ||
if err != nil { | ||
return nil, fmt.Errorf("%w: %s", err, toUnreadableBodyMessage(req, content)) | ||
} | ||
|
||
return respData, nil | ||
} | ||
|
||
// DeleteRecord deletes a DNS record. | ||
// https://vercel.com/docs/rest-api#endpoints/dns/delete-a-dns-record | ||
func (c *Client) DeleteRecord(zone string, recordID string) error { | ||
endpoint, err := c.baseURL.Parse(path.Join(c.baseURL.Path, "v2", "domains", dns01.UnFqdn(zone), "records", recordID)) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
req, err := c.newRequest(http.MethodDelete, endpoint.String(), nil) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
resp, err := c.HTTPClient.Do(req) | ||
if err != nil { | ||
return err | ||
} | ||
defer func() { _ = resp.Body.Close() }() | ||
|
||
if resp.StatusCode >= 400 { | ||
return readError(req, resp) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (c *Client) newRequest(method, reqURL string, body io.Reader) (*http.Request, error) { | ||
req, err := http.NewRequest(method, reqURL, body) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if c.teamID != "" { | ||
query := req.URL.Query() | ||
query.Add("teamId", c.teamID) | ||
req.URL.RawQuery = query.Encode() | ||
} | ||
|
||
req.Header.Set("Content-Type", "application/json") | ||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.authToken)) | ||
|
||
return req, nil | ||
} | ||
|
||
func readError(req *http.Request, resp *http.Response) error { | ||
content, err := io.ReadAll(resp.Body) | ||
if err != nil { | ||
return errors.New(toUnreadableBodyMessage(req, content)) | ||
} | ||
|
||
var errInfo APIErrorResponse | ||
err = json.Unmarshal(content, &errInfo) | ||
if err != nil { | ||
return fmt.Errorf("API Error unmarshaling error: %w: %s", err, toUnreadableBodyMessage(req, content)) | ||
} | ||
|
||
return fmt.Errorf("HTTP %d: %w", resp.StatusCode, errInfo.Error) | ||
} | ||
|
||
func toUnreadableBodyMessage(req *http.Request, rawBody []byte) string { | ||
return fmt.Sprintf("the request %s sent a response with a body which is an invalid format: %q", req.URL, string(rawBody)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
package internal | ||
|
||
import ( | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"net/http/httptest" | ||
"net/url" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func setup(t *testing.T) (*Client, *http.ServeMux) { | ||
t.Helper() | ||
|
||
mux := http.NewServeMux() | ||
server := httptest.NewServer(mux) | ||
t.Cleanup(server.Close) | ||
|
||
client := NewClient("secret", "123") | ||
|
||
client.HTTPClient = server.Client() | ||
client.baseURL, _ = url.Parse(server.URL) | ||
|
||
return client, mux | ||
} | ||
|
||
func TestClient_CreateRecord(t *testing.T) { | ||
client, mux := setup(t) | ||
|
||
mux.HandleFunc("/v2/domains/example.com/records", func(rw http.ResponseWriter, req *http.Request) { | ||
if req.Method != http.MethodPost { | ||
http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) | ||
return | ||
} | ||
|
||
auth := req.Header.Get("Authorization") | ||
if auth != "Bearer secret" { | ||
http.Error(rw, fmt.Sprintf("invalid API token: %s", auth), http.StatusUnauthorized) | ||
return | ||
} | ||
|
||
teamID := req.URL.Query().Get("teamId") | ||
if teamID != "123" { | ||
http.Error(rw, fmt.Sprintf("invalid team ID: %s", teamID), http.StatusUnauthorized) | ||
return | ||
} | ||
|
||
reqBody, err := io.ReadAll(req.Body) | ||
if err != nil { | ||
http.Error(rw, err.Error(), http.StatusInternalServerError) | ||
return | ||
} | ||
|
||
expectedReqBody := `{"name":"_acme-challenge.example.com.","type":"TXT","value":"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI","ttl":60}` | ||
assert.Equal(t, expectedReqBody, string(reqBody)) | ||
|
||
rw.WriteHeader(http.StatusOK) | ||
_, err = fmt.Fprintf(rw, `{ | ||
"uid": "9e2eab60-0ba5-4dff-b481-2999c9764b84", | ||
"updated": 1 | ||
}`) | ||
if err != nil { | ||
http.Error(rw, err.Error(), http.StatusInternalServerError) | ||
return | ||
} | ||
}) | ||
|
||
record := Record{ | ||
Name: "_acme-challenge.example.com.", | ||
Type: "TXT", | ||
Value: "w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI", | ||
TTL: 60, | ||
} | ||
|
||
resp, err := client.CreateRecord("example.com.", record) | ||
require.NoError(t, err) | ||
|
||
expected := &CreateRecordResponse{ | ||
UID: "9e2eab60-0ba5-4dff-b481-2999c9764b84", | ||
Updated: 1, | ||
} | ||
|
||
assert.Equal(t, expected, resp) | ||
} | ||
|
||
func TestClient_DeleteRecord(t *testing.T) { | ||
client, mux := setup(t) | ||
|
||
mux.HandleFunc("/v2/domains/example.com/records/1234567", func(rw http.ResponseWriter, req *http.Request) { | ||
if req.Method != http.MethodDelete { | ||
http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) | ||
return | ||
} | ||
auth := req.Header.Get("Authorization") | ||
if auth != "Bearer secret" { | ||
http.Error(rw, fmt.Sprintf("invalid API token: %s", auth), http.StatusUnauthorized) | ||
return | ||
} | ||
|
||
teamID := req.URL.Query().Get("teamId") | ||
if teamID != "123" { | ||
http.Error(rw, fmt.Sprintf("invalid team ID: %s", teamID), http.StatusUnauthorized) | ||
return | ||
} | ||
|
||
rw.WriteHeader(http.StatusOK) | ||
}) | ||
|
||
err := client.DeleteRecord("example.com.", "1234567") | ||
require.NoError(t, err) | ||
} |
Oops, something went wrong.