mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-26 02:30:57 +00:00

This method uses `path.Join` to build the URL. Turns out with 1.24 this started stripping consecutive "/" characters, so "http://..." in baseURL becomes "http:/...". Also, `c.Tailnet` is a function that returns `c.tailnet`. Using it as a path element would encode as a pointer instead of the tailnet name. Finally, provide a way to prevent escaping of path elements e.g. for `?` in `acl?details=1`. Updates #15015 Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
207 lines
6.3 KiB
Go
207 lines
6.3 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build go1.19
|
|
|
|
// Package tailscale contains a Go client for the Tailscale control plane API.
|
|
//
|
|
// This package is only intended for internal and transitional use.
|
|
//
|
|
// Deprecated: the official control plane client is available at
|
|
// tailscale.com/client/tailscale/v2.
|
|
package tailscale
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
)
|
|
|
|
// I_Acknowledge_This_API_Is_Unstable must be set true to use this package
|
|
// for now. This package is being replaced by tailscale.com/client/tailscale/v2.
|
|
var I_Acknowledge_This_API_Is_Unstable = false
|
|
|
|
// TODO: use url.PathEscape() for deviceID and tailnets when constructing requests.
|
|
|
|
const defaultAPIBase = "https://api.tailscale.com"
|
|
|
|
// maxSize is the maximum read size (10MB) of responses from the server.
|
|
const maxReadSize = 10 << 20
|
|
|
|
// 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.
|
|
//
|
|
// Deprecated: use tailscale.com/client/tailscale/v2 instead.
|
|
type Client struct {
|
|
// tailnet is the globally unique identifier for a Tailscale network, such
|
|
// as "example.com" or "user@gmail.com".
|
|
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
|
|
|
|
// UserAgent optionally specifies an alternate User-Agent header
|
|
UserAgent string
|
|
}
|
|
|
|
func (c *Client) httpClient() *http.Client {
|
|
if c.HTTPClient != nil {
|
|
return c.HTTPClient
|
|
}
|
|
return http.DefaultClient
|
|
}
|
|
|
|
// BuildURL builds a url to http(s)://<apiserver>/api/v2/<slash-separated-pathElements>
|
|
// using the given pathElements. It url escapes each path element, so the
|
|
// caller doesn't need to worry about that. The last item of pathElements can
|
|
// be of type url.Values to add a query string to the URL.
|
|
//
|
|
// For example, BuildURL(devices, 5) with the default server URL would result in
|
|
// https://api.tailscale.com/api/v2/devices/5.
|
|
func (c *Client) BuildURL(pathElements ...any) string {
|
|
elem := make([]string, 1, len(pathElements)+1)
|
|
elem[0] = "/api/v2"
|
|
var query string
|
|
for i, pathElement := range pathElements {
|
|
if uv, ok := pathElement.(url.Values); ok && i == len(pathElements)-1 {
|
|
query = uv.Encode()
|
|
} else {
|
|
elem = append(elem, url.PathEscape(fmt.Sprint(pathElement)))
|
|
}
|
|
}
|
|
url := c.baseURL() + path.Join(elem...)
|
|
if query != "" {
|
|
url += "?" + query
|
|
}
|
|
return url
|
|
}
|
|
|
|
// BuildTailnetURL builds a url to http(s)://<apiserver>/api/v2/tailnet/<tailnet>/<slash-separated-pathElements>
|
|
// using the given pathElements. It url escapes each path element, so the
|
|
// caller doesn't need to worry about that. The last item of pathElements can
|
|
// be of type url.Values to add a query string to the URL.
|
|
//
|
|
// For example, BuildTailnetURL(policy, validate) with the default server URL and a tailnet of "example.com"
|
|
// would result in https://api.tailscale.com/api/v2/tailnet/example.com/policy/validate.
|
|
func (c *Client) BuildTailnetURL(pathElements ...any) string {
|
|
allElements := make([]any, 2, len(pathElements)+2)
|
|
allElements[0] = "tailnet"
|
|
allElements[1] = c.tailnet
|
|
allElements = append(allElements, pathElements...)
|
|
return c.BuildURL(allElements...)
|
|
}
|
|
|
|
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.
|
|
//
|
|
// Deprecated: use tailscale.com/client/tailscale/v2 instead.
|
|
func NewClient(tailnet string, auth AuthMethod) *Client {
|
|
return &Client{
|
|
tailnet: tailnet,
|
|
auth: auth,
|
|
UserAgent: "tailscale-client-oss",
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
c.setAuth(req)
|
|
if c.UserAgent != "" {
|
|
req.Header.Set("User-Agent", c.UserAgent)
|
|
}
|
|
return c.httpClient().Do(req)
|
|
}
|
|
|
|
// sendRequest add the authentication key to the request and sends it. It
|
|
// receives the response and reads up to 10MB of it.
|
|
func (c *Client) sendRequest(req *http.Request) ([]byte, *http.Response, error) {
|
|
resp, err := c.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+1)
|
|
b, err := io.ReadAll(body)
|
|
if len(b) > maxReadSize {
|
|
err = errors.New("API response too large")
|
|
}
|
|
return b, resp, err
|
|
}
|
|
|
|
// ErrResponse is the HTTP error returned by the Tailscale server.
|
|
type ErrResponse struct {
|
|
Status int
|
|
Message string
|
|
}
|
|
|
|
func (e ErrResponse) Error() string {
|
|
return fmt.Sprintf("Status: %d, Message: %q", e.Status, e.Message)
|
|
}
|
|
|
|
// HandleErrorResponse decodes the error message from the server and returns
|
|
// an ErrResponse from it.
|
|
//
|
|
// Deprecated: use tailscale.com/client/tailscale/v2 instead.
|
|
func HandleErrorResponse(b []byte, resp *http.Response) error {
|
|
var errResp ErrResponse
|
|
if err := json.Unmarshal(b, &errResp); err != nil {
|
|
return fmt.Errorf("json.Unmarshal %q: %w", b, err)
|
|
}
|
|
errResp.Status = resp.StatusCode
|
|
return errResp
|
|
}
|