diff --git a/cmd/cloudflared/tunnel/cmd.go b/cmd/cloudflared/tunnel/cmd.go index 508e71d0..9074a0f6 100644 --- a/cmd/cloudflared/tunnel/cmd.go +++ b/cmd/cloudflared/tunnel/cmd.go @@ -127,7 +127,9 @@ var ( "most likely you already have a conflicting record there. You can also rerun this command with --%s to overwrite "+ "any existing DNS records for this hostname.", overwriteDNSFlag) deprecatedClassicTunnelErr = fmt.Errorf("Classic tunnels have been deprecated, please use Named Tunnels. (https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/tunnel-guide/)") - nonSecretFlagsList = []string{ + // TODO: TUN-8756 the list below denotes the flags that do not possess any kind of sensitive information + // however this approach is not maintainble in the long-term. + nonSecretFlagsList = []string{ "config", "autoupdate-freq", "no-autoupdate", diff --git a/diagnostic/client.go b/diagnostic/client.go new file mode 100644 index 00000000..f22830bb --- /dev/null +++ b/diagnostic/client.go @@ -0,0 +1,101 @@ +package diagnostic + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strconv" + + "github.com/cloudflare/cloudflared/logger" +) + +const configurationEndpoint = "diag/configuration" + +type httpClient struct { + http.Client + baseURL url.URL +} + +func NewHTTPClient(baseURL url.URL) *httpClient { + httpTransport := http.Transport{ + TLSHandshakeTimeout: defaultTimeout, + ResponseHeaderTimeout: defaultTimeout, + } + + return &httpClient{ + http.Client{ + Transport: &httpTransport, + Timeout: defaultTimeout, + }, + baseURL, + } +} + +func (client *httpClient) GET(ctx context.Context, url string) (*http.Response, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("error creating GET request: %w", err) + } + + req.Header.Add("Accept", "application/json;version=1") + + response, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("error GET request: %w", err) + } + + return response, nil +} + +type LogConfiguration struct { + logFile string + logDirectory string + uid int // the uid of the user that started cloudflared +} + +func (client *httpClient) GetLogConfiguration(ctx context.Context) (*LogConfiguration, error) { + endpoint, err := url.JoinPath(client.baseURL.String(), configurationEndpoint) + if err != nil { + return nil, fmt.Errorf("error parsing URL: %w", err) + } + + response, err := client.GET(ctx, endpoint) + if err != nil { + return nil, err + } + + defer response.Body.Close() + + var data map[string]string + if err := json.NewDecoder(response.Body).Decode(&data); err != nil { + return nil, fmt.Errorf("failed to decode body: %w", err) + } + + uidStr, exists := data[configurationKeyUID] + if !exists { + return nil, ErrKeyNotFound + } + + uid, err := strconv.Atoi(uidStr) + if err != nil { + return nil, fmt.Errorf("error convertin pid to int: %w", err) + } + + logFile, exists := data[logger.LogFileFlag] + if exists { + return &LogConfiguration{logFile, "", uid}, nil + } + + logDirectory, exists := data[logger.LogDirectoryFlag] + if exists { + return &LogConfiguration{"", logDirectory, uid}, nil + } + + return nil, ErrKeyNotFound +} + +type HTTPClient interface { + GetLogConfiguration(ctx context.Context) (LogConfiguration, error) +} diff --git a/diagnostic/consts.go b/diagnostic/consts.go index 0fd2b574..1a78c888 100644 --- a/diagnostic/consts.go +++ b/diagnostic/consts.go @@ -3,10 +3,12 @@ package diagnostic import "time" const ( - defaultCollectorTimeout = time.Second * 10 // This const define the timeout value of a collector operation. - collectorField = "collector" // used for logging purposes - systemCollectorName = "system" // used for logging purposes - tunnelStateCollectorName = "tunnelState" // used for logging purposes - configurationCollectorName = "configuration" // used for logging purposes - configurationKeyUid = "uid" + defaultCollectorTimeout = time.Second * 10 // This const define the timeout value of a collector operation. + collectorField = "collector" // used for logging purposes + systemCollectorName = "system" // used for logging purposes + tunnelStateCollectorName = "tunnelState" // used for logging purposes + configurationCollectorName = "configuration" // used for logging purposes + defaultTimeout = 15 * time.Second // timeout for the collectors + twoWeeksOffset = -14 * 24 * time.Hour // maximum offset for the logs + configurationKeyUID = "uid" // Key used to set and get the UID value from the configuration map ) diff --git a/diagnostic/error.go b/diagnostic/error.go index 88884a48..32cca31d 100644 --- a/diagnostic/error.go +++ b/diagnostic/error.go @@ -5,12 +5,17 @@ import ( ) var ( + // Error used when there is no log directory available. + ErrManagedLogNotFound = errors.New("managed log directory not found") + // Error used when one key is not found. + ErrMustNotBeEmpty = errors.New("provided argument is empty") // Error used when parsing the fields of the output of collector. ErrInsufficientLines = errors.New("insufficient lines") // Error used when parsing the lines of the output of collector. ErrInsuficientFields = errors.New("insufficient fields") // Error used when given key is not found while parsing KV. ErrKeyNotFound = errors.New("key not found") - // Error used when tehre is no disk volume information available - ErrNoVolumeFound = errors.New("No disk volume information found") + // Error used when there is no disk volume information available. + ErrNoVolumeFound = errors.New("no disk volume information found") + ErrNoPathAvailable = errors.New("no path available") ) diff --git a/diagnostic/handlers.go b/diagnostic/handlers.go index 6eb2ed74..bdb17199 100644 --- a/diagnostic/handlers.go +++ b/diagnostic/handlers.go @@ -166,7 +166,7 @@ func (handler *Handler) ConfigurationHandler(writer http.ResponseWriter, _ *http // The UID is included to help the // diagnostic tool to understand // if this instance is managed or not. - flags[configurationKeyUid] = strconv.Itoa(os.Getuid()) + flags[configurationKeyUID] = strconv.Itoa(os.Getuid()) encoder := json.NewEncoder(writer) err := encoder.Encode(flags) diff --git a/diagnostic/log_collector.go b/diagnostic/log_collector.go new file mode 100644 index 00000000..cdf559e7 --- /dev/null +++ b/diagnostic/log_collector.go @@ -0,0 +1,34 @@ +package diagnostic + +import ( + "context" +) + +// Represents the path of the log file or log directory. +// This struct is meant to give some ergonimics regarding +// the logging information. +type LogInformation struct { + path string // path to a file or directory + wasCreated bool // denotes if `path` was created + isDirectory bool // denotes if `path` is a directory +} + +func NewLogInformation( + path string, + wasCreated bool, + isDirectory bool, +) *LogInformation { + return &LogInformation{ + path, + wasCreated, + isDirectory, + } +} + +type LogCollector interface { + // This function is responsible for returning a path to a single file + // whose contents are the logs of a cloudflared instance. + // A new file may be create by a LogCollector, thus, its the caller + // responsibility to remove the newly create file. + Collect(ctx context.Context) (*LogInformation, error) +} diff --git a/diagnostic/log_collector_host.go b/diagnostic/log_collector_host.go new file mode 100644 index 00000000..66981ef6 --- /dev/null +++ b/diagnostic/log_collector_host.go @@ -0,0 +1,73 @@ +package diagnostic + +import ( + "context" + "fmt" + "os" + "path/filepath" + "runtime" +) + +const ( + linuxManagedLogsPath = "/var/log/cloudflared.err" + darwinManagedLogsPath = "/Library/Logs/com.cloudflare.cloudflared.err.log" +) + +type HostLogCollector struct { + client HTTPClient +} + +func NewHostLogCollector(client HTTPClient) *HostLogCollector { + return &HostLogCollector{ + client, + } +} + +func getServiceLogPath() (string, error) { + switch runtime.GOOS { + case "darwin": + { + path := darwinManagedLogsPath + if _, err := os.Stat(path); err == nil { + return path, nil + } + + userHomeDir, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("error getting user home: %w", err) + } + + return filepath.Join(userHomeDir, darwinManagedLogsPath), nil + } + case "linux": + { + return linuxManagedLogsPath, nil + } + default: + return "", ErrManagedLogNotFound + } +} + +func (collector *HostLogCollector) Collect(ctx context.Context) (*LogInformation, error) { + logConfiguration, err := collector.client.GetLogConfiguration(ctx) + if err != nil { + return nil, fmt.Errorf("error getting log configuration: %w", err) + } + + if logConfiguration.uid == 0 { + path, err := getServiceLogPath() + if err != nil { + return nil, err + } + + return NewLogInformation(path, false, false), nil + } + + if logConfiguration.logFile != "" { + return NewLogInformation(logConfiguration.logFile, false, false), nil + } else if logConfiguration.logDirectory != "" { + return NewLogInformation(logConfiguration.logDirectory, false, true), nil + } + + return nil, ErrMustNotBeEmpty +}