2022-06-01 23:05:05 +00:00
|
|
|
package service
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2023-04-27 11:44:39 +00:00
|
|
|
"crypto/tls"
|
2023-08-24 16:04:27 +00:00
|
|
|
"encoding/json"
|
2022-10-10 20:15:35 +00:00
|
|
|
"errors"
|
2022-06-01 23:05:05 +00:00
|
|
|
"fmt"
|
|
|
|
"net/http"
|
2023-08-24 16:04:27 +00:00
|
|
|
"time"
|
2022-09-23 19:18:19 +00:00
|
|
|
|
2023-08-24 16:04:27 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/pkg/retry"
|
2022-09-23 19:18:19 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
2023-05-15 20:00:52 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
2023-10-02 13:27:18 +00:00
|
|
|
"github.com/rs/zerolog/log"
|
2022-06-01 23:05:05 +00:00
|
|
|
)
|
|
|
|
|
2022-09-15 16:12:50 +00:00
|
|
|
// Device client is used consume the `device/...` endpoints and meant to be used by Fleet Desktop
|
2022-06-01 23:05:05 +00:00
|
|
|
type DeviceClient struct {
|
|
|
|
*baseClient
|
2023-04-27 11:44:39 +00:00
|
|
|
|
|
|
|
// fleetAlternativeBrowserHost is an alternative host to use for the Fleet Desktop URLs generated for the browser.
|
|
|
|
//
|
|
|
|
// This is needed when the host that Orbit will connect to is different from the host that will connect via the browser.
|
|
|
|
fleetAlternativeBrowserHost string
|
2023-09-26 17:50:02 +00:00
|
|
|
|
|
|
|
// if set and a request fails with ErrUnauthenticated, the client will call
|
|
|
|
// this function to get a fresh token and retry if it returns a different,
|
|
|
|
// non-empty token.
|
|
|
|
invalidTokenRetryFunc func() string
|
2022-10-10 20:15:35 +00:00
|
|
|
}
|
|
|
|
|
2023-04-27 11:44:39 +00:00
|
|
|
// NewDeviceClient instantiates a new client to perform requests against device endpoints.
|
|
|
|
func NewDeviceClient(addr string, insecureSkipVerify bool, rootCA string, fleetClientCrt *tls.Certificate, fleetAlternativeBrowserHost string) (*DeviceClient, error) {
|
2022-10-10 20:15:35 +00:00
|
|
|
capabilities := fleet.CapabilityMap{}
|
2023-04-27 11:44:39 +00:00
|
|
|
baseClient, err := newBaseClient(addr, insecureSkipVerify, rootCA, "", fleetClientCrt, capabilities)
|
2022-10-10 20:15:35 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &DeviceClient{
|
2023-04-27 11:44:39 +00:00
|
|
|
baseClient: baseClient,
|
|
|
|
fleetAlternativeBrowserHost: fleetAlternativeBrowserHost,
|
2022-10-10 20:15:35 +00:00
|
|
|
}, nil
|
2022-06-01 23:05:05 +00:00
|
|
|
}
|
|
|
|
|
2023-09-26 17:50:02 +00:00
|
|
|
// WithInvalidTokenRetry sets the function to call if a request fails with
|
|
|
|
// ErrUnauthenticated. The client will call this function to get a fresh token
|
|
|
|
// and retry if it returns a different, non-empty token.
|
|
|
|
func (dc *DeviceClient) WithInvalidTokenRetry(fn func() string) {
|
2023-10-02 13:27:18 +00:00
|
|
|
log.Debug().Msg("setting invalid token retry hook")
|
2023-09-26 17:50:02 +00:00
|
|
|
dc.invalidTokenRetryFunc = fn
|
|
|
|
}
|
|
|
|
|
|
|
|
// request performs the request, resolving the pathFmt that should contain a %s
|
|
|
|
// verb to be replaced with the token, or no verb at all if the token is "-"
|
|
|
|
// (the pathFmt is used as-is as path). It will retry if the request fails due
|
|
|
|
// to an invalid token and the invalidTokenRetryFunc field is set.
|
|
|
|
func (dc *DeviceClient) request(verb, pathFmt, token, query string, params interface{}, responseDest interface{}) error {
|
2023-10-02 13:27:18 +00:00
|
|
|
const maxAttempts = 4
|
2023-09-26 17:50:02 +00:00
|
|
|
var attempt int
|
|
|
|
for {
|
|
|
|
attempt++
|
|
|
|
|
|
|
|
path := pathFmt
|
|
|
|
if token != "-" {
|
|
|
|
path = fmt.Sprintf(pathFmt, token)
|
|
|
|
}
|
|
|
|
reqErr := dc.requestAttempt(verb, path, query, params, responseDest)
|
|
|
|
if attempt >= maxAttempts || dc.invalidTokenRetryFunc == nil || token == "-" || !errors.Is(reqErr, ErrUnauthenticated) {
|
|
|
|
// no retry possible, return the result
|
2023-10-02 13:27:18 +00:00
|
|
|
if reqErr != nil {
|
|
|
|
log.Debug().Msgf("not retrying API error; attempt=%d, hook set=%t, token unset=%t, error is auth=%t",
|
|
|
|
attempt, dc.invalidTokenRetryFunc != nil, token == "-", errors.Is(reqErr, ErrUnauthenticated))
|
|
|
|
}
|
2023-09-26 17:50:02 +00:00
|
|
|
return reqErr
|
|
|
|
}
|
|
|
|
|
2023-10-02 13:27:18 +00:00
|
|
|
delay := time.Duration(attempt) * time.Second
|
|
|
|
log.Debug().Msgf("retrying API error in %s", delay)
|
|
|
|
time.Sleep(delay)
|
2023-09-26 17:50:02 +00:00
|
|
|
newToken := dc.invalidTokenRetryFunc()
|
2023-10-02 13:27:18 +00:00
|
|
|
log.Debug().Msgf("retrying API error; token is different=%t", newToken != "" && newToken != token)
|
2023-09-26 17:50:02 +00:00
|
|
|
if newToken != "" {
|
|
|
|
token = newToken
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (dc *DeviceClient) requestAttempt(verb string, path string, query string, params interface{}, responseDest interface{}) error {
|
2022-06-01 23:05:05 +00:00
|
|
|
var bodyBytes []byte
|
2023-08-24 16:04:27 +00:00
|
|
|
var err error
|
|
|
|
if params != nil {
|
|
|
|
bodyBytes, err = json.Marshal(params)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("making request json marshalling : %w", err)
|
|
|
|
}
|
|
|
|
}
|
2022-06-01 23:05:05 +00:00
|
|
|
request, err := http.NewRequest(
|
|
|
|
verb,
|
|
|
|
dc.url(path, query).String(),
|
|
|
|
bytes.NewBuffer(bodyBytes),
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
add headers denoting capabilities between fleet server / desktop / orbit (#7833)
This adds a new mechanism to allow us to handle compatibility issues between Orbit, Fleet Server and Fleet Desktop.
The general idea is to _always_ send a custom header of the form:
```
fleet-capabilities-header = "X-Fleet-Capabilities:" capabilities
capabilities = capability * (,)
capability = string
```
Both from the server to the clients (Orbit, Fleet Desktop) and vice-versa. For an example, see: https://github.com/fleetdm/fleet/commit/8c0bbdd291f54e03e19766bcdfead0fb8067f60c
Also, the following applies:
- Backwards compat: if the header is not present, assume that orbit/fleet doesn't have the capability
- The current capabilities endpoint will be removed
### Motivation
This solution is trying to solve the following problems:
- We have three independent processes communicating with each other (Fleet Desktop, Orbit and Fleet Server). Each process can be updated independently, and therefore we need a way for each process to know what features are supported by its peers.
- We originally implemented a dedicated API endpoint in the server that returned a list of the capabilities (or "features") enabled, we found this, and any other server-only solution (like API versioning) to be insufficient because:
- There are cases in which the server also needs to know which features are supported by its clients
- Clients needed to poll for changes to detect if the capabilities supported by the server change, by sending the capabilities on each request we have a much cleaner way to handling different responses.
- We are also introducing an unauthenticated endpoint to get the server features, this gives us flexibility if we need to implement different authentication mechanisms, and was one of the pitfalls of the first implementation.
Related to https://github.com/fleetdm/fleet/issues/7929
2022-09-26 10:53:53 +00:00
|
|
|
dc.setClientCapabilitiesHeader(request)
|
2022-06-01 23:05:05 +00:00
|
|
|
response, err := dc.http.Do(request)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("%s %s: %w", verb, path, err)
|
|
|
|
}
|
|
|
|
defer response.Body.Close()
|
|
|
|
|
|
|
|
return dc.parseResponse(verb, path, response, responseDest)
|
|
|
|
}
|
|
|
|
|
2023-04-27 11:44:39 +00:00
|
|
|
// BrowserTransparencyURL returns a URL for the browser that the server
|
|
|
|
// will use to redirect to the transparency URL configured by the user.
|
|
|
|
func (dc *DeviceClient) BrowserTransparencyURL(token string) string {
|
|
|
|
transparencyURL := dc.baseClient.url("/api/latest/fleet/device/"+token+"/transparency", "")
|
|
|
|
if dc.fleetAlternativeBrowserHost != "" {
|
|
|
|
transparencyURL.Host = dc.fleetAlternativeBrowserHost
|
|
|
|
}
|
|
|
|
return transparencyURL.String()
|
2022-10-10 20:15:35 +00:00
|
|
|
}
|
|
|
|
|
2023-04-27 11:44:39 +00:00
|
|
|
// BrowserDeviceURL returns the "My device" URL for the browser.
|
|
|
|
func (dc *DeviceClient) BrowserDeviceURL(token string) string {
|
|
|
|
deviceURL := dc.baseClient.url("/device/"+token, "")
|
|
|
|
if dc.fleetAlternativeBrowserHost != "" {
|
|
|
|
deviceURL.Host = dc.fleetAlternativeBrowserHost
|
|
|
|
}
|
|
|
|
return deviceURL.String()
|
2022-10-10 20:15:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// CheckToken checks if a token is valid by making an authenticated request to
|
|
|
|
// the server
|
|
|
|
func (dc *DeviceClient) CheckToken(token string) error {
|
2023-12-13 20:31:48 +00:00
|
|
|
verb, path := "HEAD", "/api/latest/fleet/device/%s/ping"
|
|
|
|
err := dc.request(verb, path, token, "", nil, nil)
|
|
|
|
|
|
|
|
if errors.As(err, ¬FoundErr{}) {
|
|
|
|
// notFound is ok, it means an old server without the auth ping endpoint,
|
|
|
|
// so we fall back to previously-used endpoint
|
|
|
|
_, err = dc.DesktopSummary(token)
|
|
|
|
}
|
2022-10-12 20:13:43 +00:00
|
|
|
return err
|
2022-10-10 20:15:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Ping sends a ping to the server using the device/ping endpoint
|
|
|
|
func (dc *DeviceClient) Ping() error {
|
|
|
|
verb, path := "HEAD", "/api/fleet/device/ping"
|
2023-09-26 17:50:02 +00:00
|
|
|
err := dc.request(verb, path, "-", "", nil, nil)
|
2022-10-10 20:15:35 +00:00
|
|
|
|
|
|
|
if err == nil || errors.Is(err, notFoundErr{}) {
|
|
|
|
// notFound is ok, it means an old server without the ping endpoint +
|
|
|
|
// capabilities header
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
2022-10-12 20:13:43 +00:00
|
|
|
|
|
|
|
func (dc *DeviceClient) getListDevicePolicies(token string) ([]*fleet.HostPolicy, error) {
|
2023-09-26 17:50:02 +00:00
|
|
|
verb, path := "GET", "/api/latest/fleet/device/%s/policies"
|
2022-10-12 20:13:43 +00:00
|
|
|
var responseBody listDevicePoliciesResponse
|
2023-09-26 17:50:02 +00:00
|
|
|
err := dc.request(verb, path, token, "", nil, &responseBody)
|
2022-10-12 20:13:43 +00:00
|
|
|
return responseBody.Policies, err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (dc *DeviceClient) getMinDesktopPayload(token string) (fleetDesktopResponse, error) {
|
2023-09-26 17:50:02 +00:00
|
|
|
verb, path := "GET", "/api/latest/fleet/device/%s/desktop"
|
2022-10-12 20:13:43 +00:00
|
|
|
var r fleetDesktopResponse
|
2023-09-26 17:50:02 +00:00
|
|
|
err := dc.request(verb, path, token, "", nil, &r)
|
2022-10-12 20:13:43 +00:00
|
|
|
return r, err
|
|
|
|
}
|
|
|
|
|
2023-05-15 20:00:52 +00:00
|
|
|
func (dc *DeviceClient) DesktopSummary(token string) (*fleetDesktopResponse, error) {
|
2022-10-12 20:13:43 +00:00
|
|
|
r, err := dc.getMinDesktopPayload(token)
|
|
|
|
if err == nil {
|
2023-05-15 20:00:52 +00:00
|
|
|
r.FailingPolicies = ptr.Uint(uintValueOrZero(r.FailingPolicies))
|
|
|
|
return &r, nil
|
2022-10-12 20:13:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if errors.Is(err, notFoundErr{}) {
|
|
|
|
policies, err := dc.getListDevicePolicies(token)
|
|
|
|
if err != nil {
|
2023-05-15 20:00:52 +00:00
|
|
|
return nil, err
|
2022-10-12 20:13:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var failingPolicies uint
|
|
|
|
for _, policy := range policies {
|
|
|
|
if policy.Response != "pass" {
|
|
|
|
failingPolicies++
|
|
|
|
}
|
|
|
|
}
|
2023-05-15 20:00:52 +00:00
|
|
|
return &fleetDesktopResponse{
|
|
|
|
DesktopSummary: fleet.DesktopSummary{
|
|
|
|
FailingPolicies: ptr.Uint(failingPolicies),
|
|
|
|
},
|
|
|
|
}, nil
|
2022-10-12 20:13:43 +00:00
|
|
|
}
|
|
|
|
|
2023-05-15 20:00:52 +00:00
|
|
|
return nil, err
|
2022-10-12 20:13:43 +00:00
|
|
|
}
|
2023-05-18 17:21:54 +00:00
|
|
|
|
|
|
|
func (dc *DeviceClient) MigrateMDM(token string) error {
|
2023-09-26 17:50:02 +00:00
|
|
|
verb, path := "POST", "/api/latest/fleet/device/%s/migrate_mdm"
|
|
|
|
return dc.request(verb, path, token, "", nil, nil)
|
2023-08-24 16:04:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (dc *DeviceClient) ReportError(token string, fleetdErr fleet.FleetdError) error {
|
2023-09-26 17:50:02 +00:00
|
|
|
verb, path := "POST", "/api/latest/fleet/device/%s/debug/errors"
|
2023-08-24 16:04:27 +00:00
|
|
|
req := fleetdErrorRequest{FleetdError: fleetdErr}
|
|
|
|
return retry.Do(
|
|
|
|
func() error {
|
2023-09-26 17:50:02 +00:00
|
|
|
err := dc.request(verb, path, token, "", req, nil)
|
2023-08-24 16:04:27 +00:00
|
|
|
scerr, ok := err.(*statusCodeErr)
|
|
|
|
|
|
|
|
// as backwards as this seems, this endpoint returns a
|
|
|
|
// `500` status code when we post an error (it might
|
|
|
|
// return `4xx` errors if the request is malformed or
|
|
|
|
// unauthenticated)
|
|
|
|
//
|
|
|
|
// see https://github.com/fleetdm/fleet/issues/13238#issuecomment-1671769460 for more details
|
|
|
|
if !ok || scerr.code != http.StatusInternalServerError {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
retry.WithMaxAttempts(3),
|
|
|
|
retry.WithInterval(15*time.Second),
|
|
|
|
)
|
2023-05-18 17:21:54 +00:00
|
|
|
}
|