From 66f9292835b279d546790bb8ce28806a78eeec6d Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Sat, 30 Apr 2022 21:45:51 -0700 Subject: [PATCH] client/tailscale: update Client API a bit Change-Id: I81aa29a8b042a247eac1941038f5d90259569941 Signed-off-by: Brad Fitzpatrick --- client/tailscale/acl.go | 14 ++--- client/tailscale/apitype/apitype.go | 2 +- client/tailscale/devices.go | 10 +-- client/tailscale/dns.go | 4 +- client/tailscale/routes.go | 4 +- client/tailscale/tailnet.go | 4 +- client/tailscale/tailscale.go | 98 +++++++++++++++++++++-------- 7 files changed, 92 insertions(+), 44 deletions(-) diff --git a/client/tailscale/acl.go b/client/tailscale/acl.go index 4a1584866..e1cee29d6 100644 --- a/client/tailscale/acl.go +++ b/client/tailscale/acl.go @@ -64,7 +64,7 @@ func (c *Client) ACL(ctx context.Context) (acl *ACL, err error) { } }() - path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl", c.BaseURL, c.Tailnet) + path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl", c.baseURL(), c.tailnet) req, err := http.NewRequestWithContext(ctx, "GET", path, nil) if err != nil { return nil, err @@ -107,7 +107,7 @@ func (c *Client) ACLHuJSON(ctx context.Context) (acl *ACLHuJSON, err error) { } }() - path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl?details=1", c.BaseURL, c.Tailnet) + path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl?details=1", c.baseURL(), c.tailnet) req, err := http.NewRequestWithContext(ctx, "GET", path, nil) if err != nil { return nil, err @@ -159,7 +159,7 @@ func (e ACLTestError) Error() string { } func (c *Client) aclPOSTRequest(ctx context.Context, body []byte, avoidCollisions bool, etag, acceptHeader string) ([]byte, string, error) { - path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl", c.BaseURL, c.Tailnet) + path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl", c.baseURL(), c.tailnet) req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewBuffer(body)) if err != nil { return nil, "", err @@ -280,7 +280,7 @@ type ACLPreview struct { } func (c *Client) previewACLPostRequest(ctx context.Context, body []byte, previewType string, previewFor string) (res *ACLPreviewResponse, err error) { - path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl/preview", c.BaseURL, c.Tailnet) + path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl/preview", c.baseURL(), c.tailnet) req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewBuffer(body)) if err != nil { return nil, err @@ -292,7 +292,7 @@ func (c *Client) previewACLPostRequest(ctx context.Context, body []byte, preview req.URL.RawQuery = q.Encode() req.Header.Set("Content-Type", "application/hujson") - req.SetBasicAuth(c.APIKey, "") + c.setAuth(req) b, resp, err := c.sendRequest(req) if err != nil { @@ -436,14 +436,14 @@ func (c *Client) ValidateACLJSON(ctx context.Context, source, dest string) (test return nil, err } - path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl/validate", c.BaseURL, c.Tailnet) + path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl/validate", c.baseURL(), c.tailnet) req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewBuffer(postData)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") - req.SetBasicAuth(c.APIKey, "") + c.setAuth(req) b, resp, err := c.sendRequest(req) if err != nil { diff --git a/client/tailscale/apitype/apitype.go b/client/tailscale/apitype/apitype.go index bd10b4d3d..d10e20533 100644 --- a/client/tailscale/apitype/apitype.go +++ b/client/tailscale/apitype/apitype.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// Package apitype contains types for the Tailscale local API. +// Package apitype contains types for the Tailscale local API and control plane API. package apitype import "tailscale.com/tailcfg" diff --git a/client/tailscale/devices.go b/client/tailscale/devices.go index b909ae309..270a974fa 100644 --- a/client/tailscale/devices.go +++ b/client/tailscale/devices.go @@ -120,7 +120,7 @@ func (c *Client) Devices(ctx context.Context, fields *DeviceFieldsOpts) (deviceL } }() - path := fmt.Sprintf("%s/api/v2/tailnet/%s/devices", c.BaseURL, c.Tailnet) + path := fmt.Sprintf("%s/api/v2/tailnet/%s/devices", c.baseURL(), c.tailnet) req, err := http.NewRequestWithContext(ctx, "GET", path, nil) if err != nil { return nil, err @@ -158,7 +158,7 @@ func (c *Client) Device(ctx context.Context, deviceID string, fields *DeviceFiel err = fmt.Errorf("tailscale.Device: %w", err) } }() - path := fmt.Sprintf("%s/api/v2/device/%s", c.BaseURL, deviceID) + path := fmt.Sprintf("%s/api/v2/device/%s", c.baseURL(), deviceID) req, err := http.NewRequestWithContext(ctx, "GET", path, nil) if err != nil { return nil, err @@ -194,7 +194,7 @@ func (c *Client) DeleteDevice(ctx context.Context, deviceID string) (err error) } }() - path := fmt.Sprintf("%s/api/v2/device/%s", c.BaseURL, url.PathEscape(deviceID)) + path := fmt.Sprintf("%s/api/v2/device/%s", c.baseURL(), url.PathEscape(deviceID)) req, err := http.NewRequestWithContext(ctx, "DELETE", path, nil) if err != nil { return err @@ -214,7 +214,7 @@ func (c *Client) DeleteDevice(ctx context.Context, deviceID string) (err error) // AuthorizeDevice marks a device as authorized. func (c *Client) AuthorizeDevice(ctx context.Context, deviceID string) error { - path := fmt.Sprintf("%s/api/v2/device/%s/authorized", c.BaseURL, url.PathEscape(deviceID)) + path := fmt.Sprintf("%s/api/v2/device/%s/authorized", c.baseURL(), url.PathEscape(deviceID)) req, err := http.NewRequestWithContext(ctx, "POST", path, strings.NewReader(`{"authorized":true}`)) if err != nil { return err @@ -242,7 +242,7 @@ func (c *Client) SetTags(ctx context.Context, deviceID string, tags []string) er if err != nil { return err } - path := fmt.Sprintf("%s/api/v2/device/%s/tags", c.BaseURL, url.PathEscape(deviceID)) + path := fmt.Sprintf("%s/api/v2/device/%s/tags", c.baseURL(), url.PathEscape(deviceID)) req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewBuffer(data)) if err != nil { return err diff --git a/client/tailscale/dns.go b/client/tailscale/dns.go index 7b156bb48..ee1f968a1 100644 --- a/client/tailscale/dns.go +++ b/client/tailscale/dns.go @@ -46,7 +46,7 @@ type DNSPreferences struct { } func (c *Client) dnsGETRequest(ctx context.Context, endpoint string) ([]byte, error) { - path := fmt.Sprintf("%s/api/v2/tailnet/%s/dns/%s", c.BaseURL, c.Tailnet, endpoint) + path := fmt.Sprintf("%s/api/v2/tailnet/%s/dns/%s", c.baseURL(), c.tailnet, endpoint) req, err := http.NewRequestWithContext(ctx, "GET", path, nil) if err != nil { return nil, err @@ -66,7 +66,7 @@ func (c *Client) dnsGETRequest(ctx context.Context, endpoint string) ([]byte, er } func (c *Client) dnsPOSTRequest(ctx context.Context, endpoint string, postData interface{}) ([]byte, error) { - path := fmt.Sprintf("%s/api/v2/tailnet/%s/dns/%s", c.BaseURL, c.Tailnet, endpoint) + path := fmt.Sprintf("%s/api/v2/tailnet/%s/dns/%s", c.baseURL(), c.tailnet, endpoint) data, err := json.Marshal(&postData) if err != nil { return nil, err diff --git a/client/tailscale/routes.go b/client/tailscale/routes.go index c00138d3d..5c8109b34 100644 --- a/client/tailscale/routes.go +++ b/client/tailscale/routes.go @@ -34,7 +34,7 @@ func (c *Client) Routes(ctx context.Context, deviceID string) (routes *Routes, e } }() - path := fmt.Sprintf("%s/api/v2/device/%s/routes", c.BaseURL, deviceID) + path := fmt.Sprintf("%s/api/v2/device/%s/routes", c.baseURL(), deviceID) req, err := http.NewRequestWithContext(ctx, "GET", path, nil) if err != nil { return nil, err @@ -74,7 +74,7 @@ func (c *Client) SetRoutes(ctx context.Context, deviceID string, subnets []netad if err != nil { return nil, err } - path := fmt.Sprintf("%s/api/v2/device/%s/routes", c.BaseURL, deviceID) + path := fmt.Sprintf("%s/api/v2/device/%s/routes", c.baseURL(), deviceID) req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewBuffer(data)) if err != nil { return nil, err diff --git a/client/tailscale/tailnet.go b/client/tailscale/tailnet.go index 79542364d..9b3ac188d 100644 --- a/client/tailscale/tailnet.go +++ b/client/tailscale/tailnet.go @@ -22,13 +22,13 @@ func (c *Client) TailnetDeleteRequest(ctx context.Context, tailnetID string) (er } }() - path := fmt.Sprintf("%s/api/v2/tailnet/%s", c.BaseURL, url.PathEscape(string(tailnetID))) + path := fmt.Sprintf("%s/api/v2/tailnet/%s", c.baseURL(), url.PathEscape(string(tailnetID))) req, err := http.NewRequestWithContext(ctx, http.MethodDelete, path, nil) if err != nil { return err } - req.SetBasicAuth(c.APIKey, "") + c.setAuth(req) b, resp, err := c.sendRequest(req) if err != nil { return err diff --git a/client/tailscale/tailscale.go b/client/tailscale/tailscale.go index 2f21f570d..572a8c7c2 100644 --- a/client/tailscale/tailscale.go +++ b/client/tailscale/tailscale.go @@ -30,45 +30,90 @@ // TODO: use url.PathEscape() for deviceID and tailnets when constructing requests. -// DefaultURL is the default base URL used for API calls. -const DefaultURL = "https://api.tailscale.com" +const defaultAPIBase = "https://api.tailscale.com" // maxSize is the maximum read size (10MB) of responses from the server. -const maxReadSize int64 = 10 * 1024 * 1024 +const maxReadSize = 10 << 20 -// Client is needed to make different API calls to the Tailscale server. -// It holds all the necessary information so that it can be reused to make -// multiple requests for the same user. -// Unless overridden, "api.tailscale.com" is the default BaseURL. +// Client makes API calls to the Tailscale control plane API server. +// +// Use NewClient to instantiate one. Exported fields should be set before +// the client is used and not changed thereafter. type Client struct { - // Tailnet is the globally unique identifier for a Tailscale network, such + // tailnet is the globally unique identifier for a Tailscale network, such // as "example.com" or "user@gmail.com". - Tailnet string - APIKey string - BaseURL string + tailnet string + // auth is the authentication method to use for this client. + // nil means none, which generally won't work, but won't crash. + auth AuthMethod + + // BaseURL optionally specifies an alternate API server to use. + // If empty, "https://api.tailscale.com" is used. + BaseURL string + + // HTTPClient optionally specifies an alternate HTTP client to use. + // If nil, http.DefaultClient is used. HTTPClient *http.Client } -// New is a convenience method for instantiating a new Client. +func (c *Client) httpClient() *http.Client { + if c.HTTPClient != nil { + return c.HTTPClient + } + return http.DefaultClient +} + +func (c *Client) baseURL() string { + if c.BaseURL != "" { + return c.BaseURL + } + return defaultAPIBase +} + +// AuthMethod is the interface for API authentication methods. +// +// Most users will use AuthKey. +type AuthMethod interface { + modifyRequest(req *http.Request) +} + +// APIKey is an AuthMethod for NewClient that authenticates requests +// using an authkey. +type APIKey string + +func (ak APIKey) modifyRequest(req *http.Request) { + req.SetBasicAuth(string(ak), "") +} + +func (c *Client) setAuth(r *http.Request) { + if c.auth != nil { + c.auth.modifyRequest(r) + } +} + +// NewClient is a convenience method for instantiating a new Client. // // tailnet is the globally unique identifier for a Tailscale network, such // as "example.com" or "user@gmail.com". // If httpClient is nil, then http.DefaultClient is used. // "api.tailscale.com" is set as the BaseURL for the returned client // and can be changed manually by the user. -func New(tailnet string, key string, httpClient *http.Client) *Client { - c := &Client{ - Tailnet: tailnet, - APIKey: key, - BaseURL: DefaultURL, - HTTPClient: httpClient, +func NewClient(tailnet string, auth AuthMethod) *Client { + return &Client{ + tailnet: tailnet, + auth: auth, } +} - if httpClient == nil { - c.HTTPClient = http.DefaultClient +func (c *Client) Tailnet() string { return c.tailnet } + +// Do sends a raw HTTP request, after adding any authentication headers. +func (c *Client) Do(req *http.Request) (*http.Response, error) { + if !I_Acknowledge_This_API_Is_Unstable { + return nil, errors.New("use of Client without setting I_Acknowledge_This_API_Is_Unstable") } - - return c + c.setAuth(req) + return c.httpClient().Do(req) } // sendRequest add the authenication key to the request and sends it. It @@ -77,16 +122,19 @@ func (c *Client) sendRequest(req *http.Request) ([]byte, *http.Response, error) if !I_Acknowledge_This_API_Is_Unstable { return nil, nil, errors.New("use of Client without setting I_Acknowledge_This_API_Is_Unstable") } - req.SetBasicAuth(c.APIKey, "") - resp, err := c.HTTPClient.Do(req) + c.setAuth(req) + resp, err := c.httpClient().Do(req) if err != nil { return nil, resp, err } defer resp.Body.Close() // Read response. Limit the response to 10MB. - body := io.LimitReader(resp.Body, maxReadSize) + body := io.LimitReader(resp.Body, maxReadSize+1) b, err := ioutil.ReadAll(body) + if len(b) > maxReadSize { + err = errors.New("API response too large") + } return b, resp, err }