This was work done Nov-Dec 2020 by @c22wen and @chungdaniel. This is just moving it to another repo. Co-Authored-By: Christina Wen <37028905+c22wen@users.noreply.github.com> Co-Authored-By: Christina Wen <christina@tailscale.com> Co-Authored-By: Daniel Chung <chungdaniel@users.noreply.github.com> Co-Authored-By: Daniel Chung <daniel@tailscale.com> Change-Id: I6da3b05b972b54771f796b5be82de5aa463635ca Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>main
parent
e3619b890c
commit
a54671529b
@ -0,0 +1,469 @@ |
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.18
|
||||
// +build go1.18
|
||||
|
||||
package tailscale |
||||
|
||||
import ( |
||||
"bytes" |
||||
"context" |
||||
"encoding/json" |
||||
"fmt" |
||||
"net/http" |
||||
|
||||
"inet.af/netaddr" |
||||
) |
||||
|
||||
// ACLRow defines a rule that grants access by a set of users or groups to a set of servers and ports.
|
||||
type ACLRow struct { |
||||
Action string `json:"action,omitempty"` // valid values: "accept"
|
||||
Users []string `json:"users,omitempty"` |
||||
Ports []string `json:"ports,omitempty"` |
||||
} |
||||
|
||||
// ACLTest defines a test for your ACLs to prevent accidental exposure or revoking of access to key servers and ports.
|
||||
type ACLTest struct { |
||||
User string `json:"user,omitempty"` // source
|
||||
Allow []string `json:"allow,omitempty"` // expected destination ip:port that user can access
|
||||
Deny []string `json:"deny,omitempty"` // expected destination ip:port that user cannot access
|
||||
} |
||||
|
||||
// ACLDetails contains all the details for an ACL.
|
||||
type ACLDetails struct { |
||||
Tests []ACLTest `json:"tests,omitempty"` |
||||
ACLs []ACLRow `json:"acls,omitempty"` |
||||
Groups map[string][]string `json:"groups,omitempty"` |
||||
TagOwners map[string][]string `json:"tagowners,omitempty"` |
||||
Hosts map[string]string `json:"hosts,omitempty"` |
||||
} |
||||
|
||||
// ACL contains an ACLDetails and metadata.
|
||||
type ACL struct { |
||||
ACL ACLDetails |
||||
ETag string // to check with version on server
|
||||
} |
||||
|
||||
// ACLHuJSON contains the HuJSON string of the ACL and metadata.
|
||||
type ACLHuJSON struct { |
||||
ACL string |
||||
Warnings []string |
||||
ETag string // to check with version on server
|
||||
} |
||||
|
||||
// ACL makes a call to the Tailscale server to get a JSON-parsed version of the ACL.
|
||||
// The JSON-parsed version of the ACL contains no comments as proper JSON does not support
|
||||
// comments.
|
||||
func (c *Client) ACL(ctx context.Context) (acl *ACL, err error) { |
||||
// Format return errors to be descriptive.
|
||||
defer func() { |
||||
if err != nil { |
||||
err = fmt.Errorf("tailscale.ACL: %w", err) |
||||
} |
||||
}() |
||||
|
||||
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 |
||||
} |
||||
req.Header.Set("Accept", "application/json") |
||||
b, resp, err := c.sendRequest(req) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
// If status code was not successful, return the error.
|
||||
// TODO: Change the check for the StatusCode to include other 2XX success codes.
|
||||
if resp.StatusCode != http.StatusOK { |
||||
return nil, handleErrorResponse(b, resp) |
||||
} |
||||
|
||||
// Otherwise, try to decode the response.
|
||||
var aclDetails ACLDetails |
||||
if err = json.Unmarshal(b, &aclDetails); err != nil { |
||||
return nil, err |
||||
} |
||||
acl = &ACL{ |
||||
ACL: aclDetails, |
||||
ETag: resp.Header.Get("ETag"), |
||||
} |
||||
return acl, nil |
||||
} |
||||
|
||||
// ACLHuJSON makes a call to the Tailscale server to get the ACL HuJSON and returns
|
||||
// it as a string.
|
||||
// HuJSON is JSON with a few modifications to make it more human-friendly. The primary
|
||||
// changes are allowing comments and trailing comments. See the following links for more info:
|
||||
// https://tailscale.com/kb/1018/acls?q=acl#tailscale-acl-policy-format
|
||||
// https://github.com/tailscale/hujson
|
||||
func (c *Client) ACLHuJSON(ctx context.Context) (acl *ACLHuJSON, err error) { |
||||
// Format return errors to be descriptive.
|
||||
defer func() { |
||||
if err != nil { |
||||
err = fmt.Errorf("tailscale.ACLHuJSON: %w", err) |
||||
} |
||||
}() |
||||
|
||||
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 |
||||
} |
||||
req.Header.Set("Accept", "application/hujson") |
||||
b, resp, err := c.sendRequest(req) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if resp.StatusCode != http.StatusOK { |
||||
return nil, handleErrorResponse(b, resp) |
||||
} |
||||
|
||||
data := struct { |
||||
ACL []byte `json:"acl"` |
||||
Warnings []string `json:"warnings"` |
||||
}{} |
||||
if err := json.Unmarshal(b, &data); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
acl = &ACLHuJSON{ |
||||
ACL: string(data.ACL), |
||||
Warnings: data.Warnings, |
||||
ETag: resp.Header.Get("ETag"), |
||||
} |
||||
return acl, nil |
||||
} |
||||
|
||||
// ACLTestFailureSummary specifies a user for which ACL tests
|
||||
// failed and the related user-friendly error messages.
|
||||
//
|
||||
// ACLTestFailureSummary specifies the JSON format sent to the
|
||||
// JavaScript client to be rendered in the HTML.
|
||||
type ACLTestFailureSummary struct { |
||||
User string `json:"user"` |
||||
Errors []string `json:"errors"` |
||||
} |
||||
|
||||
// ACLTestError is ErrResponse but with an extra field to account for ACLTestFailureSummary.
|
||||
type ACLTestError struct { |
||||
ErrResponse |
||||
Data []ACLTestFailureSummary `json:"data"` |
||||
} |
||||
|
||||
func (e ACLTestError) Error() string { |
||||
return fmt.Sprintf("%s, Data: %+v", e.ErrResponse.Error(), e.Data) |
||||
} |
||||
|
||||
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) |
||||
req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewBuffer(body)) |
||||
if err != nil { |
||||
return nil, "", err |
||||
} |
||||
|
||||
if avoidCollisions { |
||||
req.Header.Set("If-Match", etag) |
||||
} |
||||
req.Header.Set("Accept", acceptHeader) |
||||
req.Header.Set("Content-Type", "application/hujson") |
||||
b, resp, err := c.sendRequest(req) |
||||
if err != nil { |
||||
return nil, "", err |
||||
} |
||||
|
||||
// If status code was not successful, return the error.
|
||||
// TODO: Change the check for the StatusCode to include other 2XX success codes.
|
||||
if resp.StatusCode != http.StatusOK { |
||||
// check if test error
|
||||
var ate ACLTestError |
||||
if err := json.Unmarshal(b, &ate); err != nil { |
||||
return nil, "", err |
||||
} |
||||
ate.Status = resp.StatusCode |
||||
return nil, "", ate |
||||
} |
||||
return b, resp.Header.Get("ETag"), nil |
||||
} |
||||
|
||||
// SetACL sends a POST request to update the ACL according to the provided ACL object. If
|
||||
// `avoidCollisions` is true, it will use the ETag obtained in the GET request in an If-Match
|
||||
// header to check if the previously obtained ACL was the latest version and that no updates
|
||||
// were missed.
|
||||
//
|
||||
// Returns error with status code 412 if mistmached ETag and avoidCollisions is set to true.
|
||||
// Returns error if ACL has tests that fail.
|
||||
// Returns error if there are other errors with the ACL.
|
||||
func (c *Client) SetACL(ctx context.Context, acl ACL, avoidCollisions bool) (res *ACL, err error) { |
||||
// Format return errors to be descriptive.
|
||||
defer func() { |
||||
if err != nil { |
||||
err = fmt.Errorf("tailscale.SetACL: %w", err) |
||||
} |
||||
}() |
||||
postData, err := json.Marshal(acl.ACL) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
b, etag, err := c.aclPOSTRequest(ctx, postData, avoidCollisions, acl.ETag, "application/json") |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
// Otherwise, try to decode the response.
|
||||
var aclDetails ACLDetails |
||||
if err = json.Unmarshal(b, &aclDetails); err != nil { |
||||
return nil, err |
||||
} |
||||
res = &ACL{ |
||||
ACL: aclDetails, |
||||
ETag: etag, |
||||
} |
||||
return res, nil |
||||
} |
||||
|
||||
// SetACLHuJSON sends a POST request to update the ACL according to the provided ACL object. If
|
||||
// `avoidCollisions` is true, it will use the ETag obtained in the GET request in an If-Match
|
||||
// header to check if the previously obtained ACL was the latest version and that no updates
|
||||
// were missed.
|
||||
//
|
||||
// Returns error with status code 412 if mistmached ETag and avoidCollisions is set to true.
|
||||
// Returns error if the HuJSON is invalid.
|
||||
// Returns error if ACL has tests that fail.
|
||||
// Returns error if there are other errors with the ACL.
|
||||
func (c *Client) SetACLHuJSON(ctx context.Context, acl ACLHuJSON, avoidCollisions bool) (res *ACLHuJSON, err error) { |
||||
// Format return errors to be descriptive.
|
||||
defer func() { |
||||
if err != nil { |
||||
err = fmt.Errorf("tailscale.SetACLHuJSON: %w", err) |
||||
} |
||||
}() |
||||
|
||||
postData := []byte(acl.ACL) |
||||
b, etag, err := c.aclPOSTRequest(ctx, postData, avoidCollisions, acl.ETag, "application/hujson") |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
res = &ACLHuJSON{ |
||||
ACL: string(b), |
||||
ETag: etag, |
||||
} |
||||
return res, nil |
||||
} |
||||
|
||||
// UserRuleMatch specifies the source users/groups/hosts that a rule targets
|
||||
// and the destination ports that they can access.
|
||||
// LineNumber is only useful for requests provided in HuJSON form.
|
||||
// While JSON requests will have LineNumber, the value is not useful.
|
||||
type UserRuleMatch struct { |
||||
Users []string `json:"users"` |
||||
Ports []string `json:"ports"` |
||||
LineNumber int `json:"lineNumber"` |
||||
} |
||||
|
||||
// ACLPreviewResponse is the response type of previewACLPostRequest
|
||||
type ACLPreviewResponse struct { |
||||
Matches []UserRuleMatch `json:"matches"` // ACL rules that match the specified user or ipport.
|
||||
Type string `json:"type"` // The request type: currently only "user" or "ipport".
|
||||
PreviewFor string `json:"previewFor"` // A specific user or ipport.
|
||||
} |
||||
|
||||
// ACLPreview is the response type of PreviewACLForUser, PreviewACLForIPPort, PreviewACLHuJSONForUser, and PreviewACLHuJSONForIPPort
|
||||
type ACLPreview struct { |
||||
Matches []UserRuleMatch `json:"matches"` |
||||
User string `json:"user,omitempty"` // Filled if response of PreviewACLForUser or PreviewACLHuJSONForUser
|
||||
IPPort string `json:"ipport,omitempty"` // Filled if response of PreviewACLForIPPort or PreviewACLHuJSONForIPPort
|
||||
} |
||||
|
||||
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) |
||||
req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewBuffer(body)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
q := req.URL.Query() |
||||
q.Add("type", previewType) |
||||
q.Add("previewFor", previewFor) |
||||
req.URL.RawQuery = q.Encode() |
||||
|
||||
req.Header.Set("Content-Type", "application/hujson") |
||||
req.SetBasicAuth(c.APIKey, "") |
||||
|
||||
b, resp, err := c.sendRequest(req) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
// If status code was not successful, return the error.
|
||||
// TODO: Change the check for the StatusCode to include other 2XX success codes.
|
||||
if resp.StatusCode != http.StatusOK { |
||||
return nil, handleErrorResponse(b, resp) |
||||
} |
||||
if err = json.Unmarshal(b, &res); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return res, nil |
||||
} |
||||
|
||||
// PreviewACLForUser determines what rules match a given ACL for a user.
|
||||
// The ACL can be a locally modified or clean ACL obtained from server.
|
||||
//
|
||||
// Returns ACLPreview on success with matches in a slice. If there are no matches,
|
||||
// the call is still successful but Matches will be an empty slice.
|
||||
// Returns error if the provided ACL is invalid.
|
||||
func (c *Client) PreviewACLForUser(ctx context.Context, acl ACL, user string) (res *ACLPreview, err error) { |
||||
// Format return errors to be descriptive.
|
||||
defer func() { |
||||
if err != nil { |
||||
err = fmt.Errorf("tailscale.PreviewACLForUser: %w", err) |
||||
} |
||||
}() |
||||
postData, err := json.Marshal(acl.ACL) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
b, err := c.previewACLPostRequest(ctx, postData, "user", user) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return &ACLPreview{ |
||||
Matches: b.Matches, |
||||
User: b.PreviewFor, |
||||
}, nil |
||||
} |
||||
|
||||
// PreviewACLForIPPort determines what rules match a given ACL for a ipport.
|
||||
// The ACL can be a locally modified or clean ACL obtained from server.
|
||||
//
|
||||
// Returns ACLPreview on success with matches in a slice. If there are no matches,
|
||||
// the call is still successful but Matches will be an empty slice.
|
||||
// Returns error if the provided ACL is invalid.
|
||||
func (c *Client) PreviewACLForIPPort(ctx context.Context, acl ACL, ipport netaddr.IPPort) (res *ACLPreview, err error) { |
||||
// Format return errors to be descriptive.
|
||||
defer func() { |
||||
if err != nil { |
||||
err = fmt.Errorf("tailscale.PreviewACLForIPPort: %w", err) |
||||
} |
||||
}() |
||||
postData, err := json.Marshal(acl.ACL) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
b, err := c.previewACLPostRequest(ctx, postData, "ipport", ipport.String()) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return &ACLPreview{ |
||||
Matches: b.Matches, |
||||
IPPort: b.PreviewFor, |
||||
}, nil |
||||
} |
||||
|
||||
// PreviewACLHuJSONForUser determines what rules match a given ACL for a user.
|
||||
// The ACL can be a locally modified or clean ACL obtained from server.
|
||||
//
|
||||
// Returns ACLPreview on success with matches in a slice. If there are no matches,
|
||||
// the call is still successful but Matches will be an empty slice.
|
||||
// Returns error if the provided ACL is invalid.
|
||||
func (c *Client) PreviewACLHuJSONForUser(ctx context.Context, acl ACLHuJSON, user string) (res *ACLPreview, err error) { |
||||
// Format return errors to be descriptive.
|
||||
defer func() { |
||||
if err != nil { |
||||
err = fmt.Errorf("tailscale.PreviewACLHuJSONForUser: %w", err) |
||||
} |
||||
}() |
||||
postData := []byte(acl.ACL) |
||||
b, err := c.previewACLPostRequest(ctx, postData, "user", user) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return &ACLPreview{ |
||||
Matches: b.Matches, |
||||
User: b.PreviewFor, |
||||
}, nil |
||||
} |
||||
|
||||
// PreviewACLHuJSONForIPPort determines what rules match a given ACL for a ipport.
|
||||
// The ACL can be a locally modified or clean ACL obtained from server.
|
||||
//
|
||||
// Returns ACLPreview on success with matches in a slice. If there are no matches,
|
||||
// the call is still successful but Matches will be an empty slice.
|
||||
// Returns error if the provided ACL is invalid.
|
||||
func (c *Client) PreviewACLHuJSONForIPPort(ctx context.Context, acl ACLHuJSON, ipport string) (res *ACLPreview, err error) { |
||||
// Format return errors to be descriptive.
|
||||
defer func() { |
||||
if err != nil { |
||||
err = fmt.Errorf("tailscale.PreviewACLHuJSONForIPPort: %w", err) |
||||
} |
||||
}() |
||||
postData := []byte(acl.ACL) |
||||
b, err := c.previewACLPostRequest(ctx, postData, "ipport", ipport) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return &ACLPreview{ |
||||
Matches: b.Matches, |
||||
IPPort: b.PreviewFor, |
||||
}, nil |
||||
} |
||||
|
||||
// ValidateACLJSON takes in the given source and destination (in this situation,
|
||||
// it is assumed that you are checking whether the source can connect to destination)
|
||||
// and creates an ACLTest from that. It then sends the ACLTest to the control api acl
|
||||
// validate endpoint, where the test is run. It returns a nil ACLTestError pointer if
|
||||
// no test errors occur.
|
||||
func (c *Client) ValidateACLJSON(ctx context.Context, source, dest string) (testErr *ACLTestError, err error) { |
||||
// Format return errors to be descriptive.
|
||||
defer func() { |
||||
if err != nil { |
||||
err = fmt.Errorf("tailscale.ValidateACLJSON: %w", err) |
||||
} |
||||
}() |
||||
|
||||
tests := []ACLTest{ACLTest{User: source, Allow: []string{dest}}} |
||||
postData, err := json.Marshal(tests) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
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, "") |
||||
|
||||
b, resp, err := c.sendRequest(req) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if resp.StatusCode != http.StatusOK { |
||||
return nil, fmt.Errorf("control api responsed with %d status code", resp.StatusCode) |
||||
} |
||||
|
||||
// The test ran without fail
|
||||
if len(b) == 0 { |
||||
return nil, nil |
||||
} |
||||
|
||||
var res ACLTestError |
||||
// The test returned errors.
|
||||
if err = json.Unmarshal(b, &res); err != nil { |
||||
// failed to unmarshal
|
||||
return nil, err |
||||
} |
||||
return &res, nil |
||||
} |
||||
@ -0,0 +1,20 @@ |
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package apitype |
||||
|
||||
type DNSConfig struct { |
||||
Resolvers []DNSResolver `json:"resolvers"` |
||||
FallbackResolvers []DNSResolver `json:"fallbackResolvers"` |
||||
Routes map[string][]DNSResolver `json:"routes"` |
||||
Domains []string `json:"domains"` |
||||
Nameservers []string `json:"nameservers"` |
||||
Proxied bool `json:"proxied"` |
||||
PerDomain bool `json:",omitempty"` |
||||
} |
||||
|
||||
type DNSResolver struct { |
||||
Addr string `json:"addr"` |
||||
BootstrapResolution []string `json:"bootstrapResolution,omitempty"` |
||||
} |
||||
@ -0,0 +1,262 @@ |
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.18
|
||||
// +build go1.18
|
||||
|
||||
package tailscale |
||||
|
||||
import ( |
||||
"bytes" |
||||
"context" |
||||
"encoding/json" |
||||
"fmt" |
||||
"net/http" |
||||
"net/url" |
||||
"strings" |
||||
|
||||
"tailscale.com/types/opt" |
||||
) |
||||
|
||||
type GetDevicesResponse struct { |
||||
Devices []*Device `json:"devices"` |
||||
} |
||||
|
||||
type DerpRegion struct { |
||||
Preferred bool `json:"preferred,omitempty"` |
||||
LatencyMilliseconds float64 `json:"latencyMs"` |
||||
} |
||||
|
||||
type ClientConnectivity struct { |
||||
Endpoints []string `json:"endpoints"` |
||||
DERP string `json:"derp"` |
||||
MappingVariesByDestIP opt.Bool `json:"mappingVariesByDestIP"` |
||||
// DERPLatency is mapped by region name (e.g. "New York City", "Seattle").
|
||||
DERPLatency map[string]DerpRegion `json:"latency"` |
||||
ClientSupports map[string]opt.Bool `json:"clientSupports"` |
||||
} |
||||
|
||||
type Device struct { |
||||
// Addresses is a list of the devices's Tailscale IP addresses.
|
||||
// It's currently just 1 element, the 100.x.y.z Tailscale IP.
|
||||
Addresses []string `json:"addresses"` |
||||
DeviceID string `json:"id"` |
||||
User string `json:"user"` |
||||
Name string `json:"name"` |
||||
Hostname string `json:"hostname"` |
||||
|
||||
ClientVersion string `json:"clientVersion"` // Empty for external devices.
|
||||
UpdateAvailable bool `json:"updateAvailable"` // Empty for external devices.
|
||||
OS string `json:"os"` |
||||
Created string `json:"created"` // Empty for external devices.
|
||||
LastSeen string `json:"lastSeen"` |
||||
KeyExpiryDisabled bool `json:"keyExpiryDisabled"` |
||||
Expires string `json:"expires"` |
||||
Authorized bool `json:"authorized"` |
||||
IsExternal bool `json:"isExternal"` |
||||
MachineKey string `json:"machineKey"` // Empty for external devices.
|
||||
NodeKey string `json:"nodeKey"` |
||||
|
||||
// BlocksIncomingConnections is configured via the device's
|
||||
// Tailscale client preferences. This field is only reported
|
||||
// to the API starting with Tailscale 1.3.x clients.
|
||||
BlocksIncomingConnections bool `json:"blocksIncomingConnections"` |
||||
|
||||
// The following fields are not included by default:
|
||||
|
||||
// EnabledRoutes are the previously-approved subnet routes
|
||||
// (e.g. "192.168.4.16/24", "10.5.2.4/32").
|
||||
EnabledRoutes []string `json:"enabledRoutes"` // Empty for external devices.
|
||||
// AdvertisedRoutes are the subnets (both enabled and not enabled)
|
||||
// being requested from the node.
|
||||
AdvertisedRoutes []string `json:"advertisedRoutes"` // Empty for external devices.
|
||||
|
||||
ClientConnectivity *ClientConnectivity `json:"clientConnectivity"` |
||||
} |
||||
|
||||
// DeviceFieldsOpts determines which fields should be returned in the response.
|
||||
//
|
||||
// Please only use DeviceAllFields and DeviceDefaultFields.
|
||||
// Other DeviceFieldsOpts are not supported.
|
||||
//
|
||||
// TODO: Support other DeviceFieldsOpts.
|
||||
// In the future, users should be able to create their own DeviceFieldsOpts
|
||||
// as valid arguments by setting the fields they want returned to a "non-nil"
|
||||
// value. For example, DeviceFieldsOpts{NodeID: "true"} should only return NodeIDs.
|
||||
type DeviceFieldsOpts Device |
||||
|
||||
func (d *DeviceFieldsOpts) addFieldsToQueryParameter() string { |
||||
if d == DeviceDefaultFields || d == nil { |
||||
return "default" |
||||
} |
||||
if d == DeviceAllFields { |
||||
return "all" |
||||
} |
||||
|
||||
return "" |
||||
} |
||||
|
||||
var ( |
||||
DeviceAllFields = &DeviceFieldsOpts{} |
||||
|
||||
// DeviceDefaultFields specifies that the following fields are returned:
|
||||
// Addresses, NodeID, User, Name, Hostname, ClientVersion, UpdateAvailable,
|
||||
// OS, Created, LastSeen, KeyExpiryDisabled, Expires, Authorized, IsExternal
|
||||
// MachineKey, NodeKey, BlocksIncomingConnections.
|
||||
DeviceDefaultFields = &DeviceFieldsOpts{} |
||||
) |
||||
|
||||
// Devices retrieves the list of devices for a tailnet.
|
||||
//
|
||||
// See the Device structure for the list of fields hidden for external devices.
|
||||
// The optional fields parameter specifies which fields of the devices to return; currently
|
||||
// only DeviceDefaultFields (equivalent to nil) and DeviceAllFields are supported.
|
||||
// Other values are currently undefined.
|
||||
func (c *Client) Devices(ctx context.Context, fields *DeviceFieldsOpts) (deviceList []*Device, err error) { |
||||
defer func() { |
||||
if err != nil { |
||||
err = fmt.Errorf("tailscale.Devices: %w", err) |
||||
} |
||||
}() |
||||
|
||||
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 |
||||
} |
||||
// Add fields.
|
||||
fieldStr := fields.addFieldsToQueryParameter() |
||||
q := req.URL.Query() |
||||
q.Add("fields", fieldStr) |
||||
req.URL.RawQuery = q.Encode() |
||||
|
||||
b, resp, err := c.sendRequest(req) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
// If status code was not successful, return the error.
|
||||
// TODO: Change the check for the StatusCode to include other 2XX success codes.
|
||||
if resp.StatusCode != http.StatusOK { |
||||
return nil, handleErrorResponse(b, resp) |
||||
} |
||||
|
||||
var devices GetDevicesResponse |
||||
err = json.Unmarshal(b, &devices) |
||||
return devices.Devices, err |
||||
} |
||||
|
||||
// Device retrieved the details for a specific device.
|
||||
//
|
||||
// See the Device structure for the list of fields hidden for an external device.
|
||||
// The optional fields parameter specifies which fields of the devices to return; currently
|
||||
// only DeviceDefaultFields (equivalent to nil) and DeviceAllFields are supported.
|
||||
// Other values are currently undefined.
|
||||
func (c *Client) Device(ctx context.Context, deviceID string, fields *DeviceFieldsOpts) (device *Device, err error) { |
||||
defer func() { |
||||
if err != nil { |
||||
err = fmt.Errorf("tailscale.Device: %w", err) |
||||
} |
||||
}() |
||||
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 |
||||
} |
||||
|
||||
// Add fields.
|
||||
fieldStr := fields.addFieldsToQueryParameter() |
||||
q := req.URL.Query() |
||||
q.Add("fields", fieldStr) |
||||
req.URL.RawQuery = q.Encode() |
||||
|
||||
b, resp, err := c.sendRequest(req) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
// If status code was not successful, return the error.
|
||||
// TODO: Change the check for the StatusCode to include other 2XX success codes.
|
||||
if resp.StatusCode != http.StatusOK { |
||||
return nil, handleErrorResponse(b, resp) |
||||
} |
||||
|
||||
err = json.Unmarshal(b, &device) |
||||
return device, err |
||||
} |
||||
|
||||
// DeleteDevice deletes the specified device from the Client's tailnet.
|
||||
// NOTE: Only devices that belong to the Client's tailnet can be deleted.
|
||||
// Deleting external devices is not supported.
|
||||
func (c *Client) DeleteDevice(ctx context.Context, deviceID string) (err error) { |
||||
defer func() { |
||||
if err != nil { |
||||
err = fmt.Errorf("tailscale.DeleteDevice: %w", err) |
||||
} |
||||
}() |
||||
|
||||
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 |
||||
} |
||||
|
||||
b, resp, err := c.sendRequest(req) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
// If status code was not successful, return the error.
|
||||
// TODO: Change the check for the StatusCode to include other 2XX success codes.
|
||||
if resp.StatusCode != http.StatusOK { |
||||
return handleErrorResponse(b, resp) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// 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)) |
||||
req, err := http.NewRequestWithContext(ctx, "POST", path, strings.NewReader(`{"authorized":true}`)) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
b, resp, err := c.sendRequest(req) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
// If status code was not successful, return the error.
|
||||
// TODO: Change the check for the StatusCode to include other 2XX success codes.
|
||||
if resp.StatusCode != http.StatusOK { |
||||
return handleErrorResponse(b, resp) |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// SetTags updates the ACL tags on a device.
|
||||
func (c *Client) SetTags(ctx context.Context, deviceID string, tags []string) error { |
||||
params := &struct { |
||||
Tags []string `json:"tags"` |
||||
}{Tags: tags} |
||||
data, err := json.Marshal(params) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
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 |
||||
} |
||||
|
||||
b, resp, err := c.sendRequest(req) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
// If status code was not successful, return the error.
|
||||
// TODO: Change the check for the StatusCode to include other 2XX success codes.
|
||||
if resp.StatusCode != http.StatusOK { |
||||
return handleErrorResponse(b, resp) |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
@ -0,0 +1,235 @@ |
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.18
|
||||
// +build go1.18
|
||||
|
||||
package tailscale |
||||
|
||||
import ( |
||||
"bytes" |
||||
"context" |
||||
"encoding/json" |
||||
"fmt" |
||||
"net/http" |
||||
|
||||
"tailscale.com/client/tailscale/apitype" |
||||
) |
||||
|
||||
// DNSNameServers is returned when retrieving the list of nameservers.
|
||||
// It is also the structure provided when setting nameservers.
|
||||
type DNSNameServers struct { |
||||
DNS []string `json:"dns"` // DNS name servers
|
||||
} |
||||
|
||||
// DNSNameServersPostResponse is returned when setting the list of DNS nameservers.
|
||||
//
|
||||
// It includes the MagicDNS status since nameservers changes may affect MagicDNS.
|
||||
type DNSNameServersPostResponse struct { |
||||
DNS []string `json:"dns"` // DNS name servers
|
||||
MagicDNS bool `json:"magicDNS"` // whether MagicDNS is active for this tailnet (enabled + has fallback nameservers)
|
||||
} |
||||
|
||||
// DNSSearchpaths is the list of search paths for a given domain.
|
||||
type DNSSearchPaths struct { |
||||
SearchPaths []string `json:"searchPaths"` // DNS search paths
|
||||
} |
||||
|
||||
// DNSPreferences is the preferences set for a given tailnet.
|
||||
//
|
||||
// It includes MagicDNS which can be turned on or off. To enable MagicDNS,
|
||||
// there must be at least one nameserver. When all nameservers are removed,
|
||||
// MagicDNS is disabled.
|
||||
type DNSPreferences struct { |
||||
MagicDNS bool `json:"magicDNS"` // whether MagicDNS is active for this tailnet (enabled + has fallback nameservers)
|
||||
} |
||||
|
||||
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) |
||||
req, err := http.NewRequestWithContext(ctx, "GET", path, nil) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
b, resp, err := c.sendRequest(req) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
// If status code was not successful, return the error.
|
||||
// TODO: Change the check for the StatusCode to include other 2XX success codes.
|
||||
if resp.StatusCode != http.StatusOK { |
||||
return nil, handleErrorResponse(b, resp) |
||||
} |
||||
|
||||
return b, nil |
||||
} |
||||
|
||||
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) |
||||
data, err := json.Marshal(&postData) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewBuffer(data)) |
||||
req.Header.Set("Content-Type", "application/json") |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
b, resp, err := c.sendRequest(req) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
// If status code was not successful, return the error.
|
||||
// TODO: Change the check for the StatusCode to include other 2XX success codes.
|
||||
if resp.StatusCode != http.StatusOK { |
||||
return nil, handleErrorResponse(b, resp) |
||||
} |
||||
|
||||
return b, nil |
||||
} |
||||
|
||||
// DNSConfig retrieves the DNSConfig settings for a domain.
|
||||
func (c *Client) DNSConfig(ctx context.Context) (cfg *apitype.DNSConfig, err error) { |
||||
// Format return errors to be descriptive.
|
||||
defer func() { |
||||
if err != nil { |
||||
err = fmt.Errorf("tailscale.DNSConfig: %w", err) |
||||
} |
||||
}() |
||||
b, err := c.dnsGETRequest(ctx, "config") |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
var dnsResp apitype.DNSConfig |
||||
err = json.Unmarshal(b, &dnsResp) |
||||
return &dnsResp, err |
||||
} |
||||
|
||||
func (c *Client) SetDNSConfig(ctx context.Context, cfg apitype.DNSConfig) (resp *apitype.DNSConfig, err error) { |
||||
// Format return errors to be descriptive.
|
||||
defer func() { |
||||
if err != nil { |
||||
err = fmt.Errorf("tailscale.SetDNSConfig: %w", err) |
||||
} |
||||
}() |
||||
var dnsResp apitype.DNSConfig |
||||
b, err := c.dnsPOSTRequest(ctx, "config", cfg) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
err = json.Unmarshal(b, &dnsResp) |
||||
return &dnsResp, err |
||||
} |
||||
|
||||
// NameServers retrieves the list of nameservers set for a domain.
|
||||
func (c *Client) NameServers(ctx context.Context) (nameservers []string, err error) { |
||||
// Format return errors to be descriptive.
|
||||
defer func() { |
||||
if err != nil { |
||||
err = fmt.Errorf("tailscale.NameServers: %w", err) |
||||
} |
||||
}() |
||||
b, err := c.dnsGETRequest(ctx, "nameservers") |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
var dnsResp DNSNameServers |
||||
err = json.Unmarshal(b, &dnsResp) |
||||
return dnsResp.DNS, err |
||||
} |
||||
|
||||
// SetNameServers sets the list of nameservers for a tailnet to the list provided
|
||||
// by the user.
|
||||
//
|
||||
// It returns the new list of nameservers and the MagicDNS status in case it was
|
||||
// affected by the change. For example, removing all nameservers will turn off
|
||||
// MagicDNS.
|
||||
func (c *Client) SetNameServers(ctx context.Context, nameservers []string) (dnsResp *DNSNameServersPostResponse, err error) { |
||||
defer func() { |
||||
if err != nil { |
||||
err = fmt.Errorf("tailscale.SetNameServers: %w", err) |
||||
} |
||||
}() |
||||
dnsReq := DNSNameServers{DNS: nameservers} |
||||
b, err := c.dnsPOSTRequest(ctx, "nameservers", dnsReq) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
err = json.Unmarshal(b, &dnsResp) |
||||
return dnsResp, err |
||||
} |
||||
|
||||
// DNSPreferences retrieves the DNS preferences set for a tailnet.
|
||||
//
|
||||
// It returns the status of MagicDNS.
|
||||
func (c *Client) DNSPreferences(ctx context.Context) (dnsResp *DNSPreferences, err error) { |
||||
// Format return errors to be descriptive.
|
||||
defer func() { |
||||
if err != nil { |
||||
err = fmt.Errorf("tailscale.DNSPreferences: %w", err) |
||||
} |
||||
}() |
||||
b, err := c.dnsGETRequest(ctx, "preferences") |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
err = json.Unmarshal(b, &dnsResp) |
||||
return dnsResp, err |
||||
} |
||||
|
||||
// SetDNSPreferences sets the DNS preferences for a tailnet.
|
||||
//
|
||||
// MagicDNS can only be enabled when there is at least one nameserver provided.
|
||||
// When all nameservers are removed, MagicDNS is disabled and will stay disabled,
|
||||
// unless explicitly enabled by a user again.
|
||||
func (c *Client) SetDNSPreferences(ctx context.Context, magicDNS bool) (dnsResp *DNSPreferences, err error) { |
||||
defer func() { |
||||
if err != nil { |
||||
err = fmt.Errorf("tailscale.SetDNSPreferences: %w", err) |
||||
} |
||||
}() |
||||
dnsReq := DNSPreferences{MagicDNS: magicDNS} |
||||
b, err := c.dnsPOSTRequest(ctx, "preferences", dnsReq) |
||||
if err != nil { |
||||
return |
||||
} |
||||
err = json.Unmarshal(b, &dnsResp) |
||||
return dnsResp, err |
||||
} |
||||
|
||||
// SearchPaths retrieves the list of searchpaths set for a tailnet.
|
||||
func (c *Client) SearchPaths(ctx context.Context) (searchpaths []string, err error) { |
||||
defer func() { |
||||
if err != nil { |
||||
err = fmt.Errorf("tailscale.SearchPaths: %w", err) |
||||
} |
||||
}() |
||||
b, err := c.dnsGETRequest(ctx, "searchpaths") |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
var dnsResp *DNSSearchPaths |
||||
err = json.Unmarshal(b, &dnsResp) |
||||
return dnsResp.SearchPaths, err |
||||
} |
||||
|
||||
// SetSearchPaths sets the list of searchpaths for a tailnet.
|
||||
func (c *Client) SetSearchPaths(ctx context.Context, searchpaths []string) (newSearchPaths []string, err error) { |
||||
defer func() { |
||||
if err != nil { |
||||
err = fmt.Errorf("tailscale.SetSearchPaths: %w", err) |
||||
} |
||||
}() |
||||
dnsReq := DNSSearchPaths{SearchPaths: searchpaths} |
||||
b, err := c.dnsPOSTRequest(ctx, "searchpaths", dnsReq) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
var dnsResp DNSSearchPaths |
||||
err = json.Unmarshal(b, &dnsResp) |
||||
return dnsResp.SearchPaths, err |
||||
} |
||||
@ -0,0 +1,98 @@ |
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.18
|
||||
// +build go1.18
|
||||
|
||||
package tailscale |
||||
|
||||
import ( |
||||
"bytes" |
||||
"context" |
||||
"encoding/json" |
||||
"fmt" |
||||
"net/http" |
||||
|
||||
"inet.af/netaddr" |
||||
) |
||||
|
||||
// Routes contains the lists of subnet routes that are currently advertised by a device,
|
||||
// as well as the subnets that are enabled to be routed by the device.
|
||||
type Routes struct { |
||||
AdvertisedRoutes []netaddr.IPPrefix `json:"advertisedRoutes"` |
||||
EnabledRoutes []netaddr.IPPrefix `json:"enabledRoutes"` |
||||
} |
||||
|
||||
// Routes retrieves the list of subnet routes that have been enabled for a device.
|
||||
// The routes that are returned are not necessarily advertised by the device,
|
||||
// they have only been preapproved.
|
||||
func (c *Client) Routes(ctx context.Context, deviceID string) (routes *Routes, err error) { |
||||
defer func() { |
||||
if err != nil { |
||||
err = fmt.Errorf("tailscale.Routes: %w", err) |
||||
} |
||||
}() |
||||
|
||||
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 |
||||
} |
||||
|
||||
b, resp, err := c.sendRequest(req) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
// If status code was not successful, return the error.
|
||||
// TODO: Change the check for the StatusCode to include other 2XX success codes.
|
||||
if resp.StatusCode != http.StatusOK { |
||||
return nil, handleErrorResponse(b, resp) |
||||
} |
||||
|
||||
var sr Routes |
||||
err = json.Unmarshal(b, &sr) |
||||
return &sr, err |
||||
} |
||||
|
||||
type postRoutesParams struct { |
||||
Routes []netaddr.IPPrefix `json:"routes"` |
||||
} |
||||
|
||||
// SetRoutes updates the list of subnets that are enabled for a device.
|
||||
// Subnets must be parsable by inet.af/netaddr.ParseIPPrefix.
|
||||
// Subnets do not have to be currently advertised by a device, they may be pre-enabled.
|
||||
// Returns the updated list of enabled and advertised subnet routes in a *Routes object.
|
||||
func (c *Client) SetRoutes(ctx context.Context, deviceID string, subnets []netaddr.IPPrefix) (routes *Routes, err error) { |
||||
defer func() { |
||||
if err != nil { |
||||
err = fmt.Errorf("tailscale.SetRoutes: %w", err) |
||||
} |
||||
}() |
||||
params := &postRoutesParams{Routes: subnets} |
||||
data, err := json.Marshal(params) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
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 |
||||
} |
||||
|
||||
b, resp, err := c.sendRequest(req) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
// If status code was not successful, return the error.
|
||||
// TODO: Change the check for the StatusCode to include other 2XX success codes.
|
||||
if resp.StatusCode != http.StatusOK { |
||||
return nil, handleErrorResponse(b, resp) |
||||
} |
||||
|
||||
var srr *Routes |
||||
if err := json.Unmarshal(b, &srr); err != nil { |
||||
return nil, err |
||||
} |
||||
return srr, err |
||||
} |
||||
@ -0,0 +1,42 @@ |
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.18
|
||||
// +build go1.18
|
||||
|
||||
package tailscale |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"net/http" |
||||
"net/url" |
||||
) |
||||
|
||||
// TailnetDeleteRequest handles sending a DELETE request for a tailnet to control.
|
||||
func (c *Client) TailnetDeleteRequest(ctx context.Context, tailnetID string) (err error) { |
||||
defer func() { |
||||
if err != nil { |
||||
err = fmt.Errorf("tailscale.DeleteTailnet: %w", err) |
||||
} |
||||
}() |
||||
|
||||
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, "") |
||||
b, resp, err := c.sendRequest(req) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if resp.StatusCode != http.StatusOK { |
||||
return handleErrorResponse(b, resp) |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
@ -0,0 +1,112 @@ |
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.18
|
||||
// +build go1.18
|
||||
|
||||
// Package tailscale contains Go clients for the Tailscale Local API and
|
||||
// Tailscale control plane API.
|
||||
//
|
||||
// Warning: this package is in development and makes no API compatibility
|
||||
// promises as of 2022-04-29. It is subject to change at any time.
|
||||
package tailscale |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"errors" |
||||
"fmt" |
||||
"io" |
||||
"io/ioutil" |
||||
"net/http" |
||||
) |
||||
|
||||
// I_Acknowledge_This_API_Is_Unstable must be set true to use this package
|
||||
// for now. It was added 2022-04-29 when it was moved to this git repo
|
||||
// and will be removed when the public API has settled.
|
||||
//
|
||||
// TODO(bradfitz): remove this after the we're happy with the public API.
|
||||
var I_Acknowledge_This_API_Is_Unstable = false |
||||
|
||||
// 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" |
||||
|
||||
// maxSize is the maximum read size (10MB) of responses from the server.
|
||||
const maxReadSize int64 = 10 * 1024 * 1024 |
||||
|
||||
// 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.
|
||||
type Client struct { |
||||
// Tailnet is the globally unique identifier for a Tailscale network, such
|
||||
// as "example.com" or "user@gmail.com".
|
||||
Tailnet string |
||||
APIKey string |
||||
BaseURL string |
||||
HTTPClient *http.Client |
||||
} |
||||
|
||||
// New 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, |
||||
} |
||||
|
||||
if httpClient == nil { |
||||
c.HTTPClient = http.DefaultClient |
||||
} |
||||
|
||||
return c |
||||
} |
||||
|
||||
// sendRequest add the authenication 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) { |
||||
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) |
||||
if err != nil { |
||||
return nil, resp, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
// Read response. Limit the response to 10MB.
|
||||
body := io.LimitReader(resp.Body, maxReadSize) |
||||
b, err := ioutil.ReadAll(body) |
||||
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.
|
||||
func handleErrorResponse(b []byte, resp *http.Response) error { |
||||
var errResp ErrResponse |
||||
if err := json.Unmarshal(b, &errResp); err != nil { |
||||
return err |
||||
} |
||||
errResp.Status = resp.StatusCode |
||||
return errResp |
||||
} |
||||
Loading…
Reference in new issue