diff --git a/cmd/root.go b/cmd/root.go index 65262c6de..d89f8e739 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -158,22 +158,13 @@ func doImport(src source.Source, s source.Metadata, catalog *pkg.Catalog, d *dis } } - var scheme string - var hostname = appConfig.Anchore.Host - urlFields := strings.Split(hostname, "://") - if len(urlFields) > 1 { - scheme = urlFields[0] - hostname = urlFields[1] - } - c, err := anchore.NewClient(anchore.Configuration{ - Hostname: hostname, + BaseURL: appConfig.Anchore.Host, Username: appConfig.Anchore.Username, Password: appConfig.Anchore.Password, - Scheme: scheme, }) if err != nil { - return fmt.Errorf("failed to create anchore client: %+v", err) + return fmt.Errorf("unable to upload results: %w", err) } importCfg := anchore.ImportConfig{ diff --git a/internal/anchore/client.go b/internal/anchore/client.go index 3283baf71..b048fcdca 100644 --- a/internal/anchore/client.go +++ b/internal/anchore/client.go @@ -2,7 +2,11 @@ package anchore import ( "context" + "errors" "fmt" + "path" + "strings" + "unicode" "github.com/anchore/client-go/pkg/external" "github.com/anchore/syft/internal" @@ -10,11 +14,10 @@ import ( ) type Configuration struct { - Hostname string + BaseURL string Username string Password string UserAgent string - Scheme string } type Client struct { @@ -29,16 +32,16 @@ func NewClient(cfg Configuration) (*Client, error) { cfg.UserAgent = fmt.Sprintf("%s / %s %s", internal.ApplicationName, versionInfo.Version, versionInfo.Platform) } - if cfg.Scheme == "" { - cfg.Scheme = "https" + baseURL, err := prepareBaseURLForClient(cfg.BaseURL) + if err != nil { + return nil, fmt.Errorf("unable to create client: %w", err) } return &Client{ config: cfg, client: external.NewAPIClient(&external.Configuration{ - Host: cfg.Hostname, + BasePath: baseURL, UserAgent: cfg.UserAgent, - Scheme: cfg.Scheme, }), }, nil } @@ -56,3 +59,57 @@ func (c *Client) newRequestContext(parentContext context.Context) context.Contex }, ) } + +var ErrInvalidBaseURLInput = errors.New("invalid base URL input") + +func prepareBaseURLForClient(baseURL string) (string, error) { + if err := checkBaseURLInput(baseURL); err != nil { + return "", err + } + + scheme, urlWithoutScheme := splitSchemeFromURL(baseURL) + + if scheme == "" { + scheme = "http" + } + + urlWithoutScheme = path.Clean(urlWithoutScheme) + + const requiredSuffix = "v1" + if path.Base(urlWithoutScheme) != requiredSuffix { + urlWithoutScheme = path.Join(urlWithoutScheme, requiredSuffix) + } + + preparedBaseURL := scheme + "://" + urlWithoutScheme + return preparedBaseURL, nil +} + +func checkBaseURLInput(url string) error { + if url == "" { + return ErrInvalidBaseURLInput + } + + firstCharacter := rune(url[0]) + if !(unicode.IsLetter(firstCharacter)) { + return ErrInvalidBaseURLInput + } + + return nil +} + +func splitSchemeFromURL(url string) (scheme, urlWithoutScheme string) { + if hasScheme(url) { + urlParts := strings.SplitN(url, "://", 2) + scheme = urlParts[0] + urlWithoutScheme = urlParts[1] + return + } + + return "", url +} + +func hasScheme(url string) bool { + parts := strings.Split(url, "://") + + return len(parts) > 1 +} diff --git a/internal/anchore/client_test.go b/internal/anchore/client_test.go new file mode 100644 index 000000000..fc07ce708 --- /dev/null +++ b/internal/anchore/client_test.go @@ -0,0 +1,210 @@ +package anchore + +import "testing" + +func TestHasScheme(t *testing.T) { + cases := []struct { + url string + expected bool + }{ + { + url: "http://localhost", + expected: true, + }, + { + url: "https://anchore.com:8443", + expected: true, + }, + { + url: "google.com", + expected: false, + }, + { + url: "", + expected: false, + }, + } + + for _, testCase := range cases { + t.Run(testCase.url, func(t *testing.T) { + result := hasScheme(testCase.url) + + if testCase.expected != result { + t.Errorf("expected %t but got %t", testCase.expected, result) + } + }) + } +} + +func TestPrepareBaseURLForClient(t *testing.T) { + cases := []struct { + inputURL string + expectedURL string + expectedErr error + }{ + { + inputURL: "", + expectedURL: "", + expectedErr: ErrInvalidBaseURLInput, + }, + { + inputURL: "localhost", + expectedURL: "http://localhost/v1", + expectedErr: nil, + }, + { + inputURL: "https://localhost", + expectedURL: "https://localhost/v1", + expectedErr: nil, + }, + { + inputURL: "https://localhost/", + expectedURL: "https://localhost/v1", + expectedErr: nil, + }, + { + inputURL: "https://localhost/v1/", + expectedURL: "https://localhost/v1", + expectedErr: nil, + }, + { + inputURL: "https://localhost/v1//", + expectedURL: "https://localhost/v1", + expectedErr: nil, + }, + { + inputURL: "http://something.com/platform/v1/services/anchore", + expectedURL: "http://something.com/platform/v1/services/anchore/v1", + expectedErr: nil, + }, + { + inputURL: "my-host:8228", + expectedURL: "http://my-host:8228/v1", + expectedErr: nil, + }, + { + inputURL: "v1/v1", + expectedURL: "http://v1/v1", + expectedErr: nil, + }, + { + inputURL: "/v1", + expectedURL: "", + expectedErr: ErrInvalidBaseURLInput, + }, + { + inputURL: "/imports/images", + expectedURL: "", + expectedErr: ErrInvalidBaseURLInput, + }, + } + + for _, testCase := range cases { + t.Run(testCase.inputURL, func(t *testing.T) { + resultURL, err := prepareBaseURLForClient(testCase.inputURL) + if err != testCase.expectedErr { + t.Errorf("expected err to be '%v' but got '%v'", testCase.expectedErr, err) + } + + if resultURL != testCase.expectedURL { + t.Errorf("expected URL to be '%v' but got '%v'", testCase.expectedURL, resultURL) + } + }) + } +} + +func TestCheckBaseURLInput(t *testing.T) { + cases := []struct { + input string + expected error + }{ + { + input: "", + expected: ErrInvalidBaseURLInput, + }, + { + input: "x", + expected: nil, + }, + { + input: "localhost:8000", + expected: nil, + }, + { + input: ":80", + expected: ErrInvalidBaseURLInput, + }, + { + input: "/v1", + expected: ErrInvalidBaseURLInput, + }, + } + + for _, testCase := range cases { + t.Run(testCase.input, func(t *testing.T) { + resultErr := checkBaseURLInput(testCase.input) + + if testCase.expected != resultErr { + t.Errorf("expected err to be '%v' but got '%v'", testCase.expected, resultErr) + } + }) + } +} + +func TestSplitSchemeFromURL(t *testing.T) { + cases := []struct { + input string + expectedScheme string + expectedURLWithoutScheme string + }{ + { + input: "", + expectedScheme: "", + expectedURLWithoutScheme: "", + }, + { + input: "localhost", + expectedScheme: "", + expectedURLWithoutScheme: "localhost", + }, + { + input: "https://anchore.com/path", + expectedScheme: "https", + expectedURLWithoutScheme: "anchore.com/path", + }, + { + input: "tcp://host:1234", + expectedScheme: "tcp", + expectedURLWithoutScheme: "host:1234", + }, + { + input: "/hello", + expectedScheme: "", + expectedURLWithoutScheme: "/hello", + }, + { + input: "://host", + expectedScheme: "", + expectedURLWithoutScheme: "host", + }, + { + input: "http//localhost", + expectedScheme: "", + expectedURLWithoutScheme: "http//localhost", + }, + } + + for _, testCase := range cases { + t.Run(testCase.input, func(t *testing.T) { + resultScheme, resultURLWithoutScheme := splitSchemeFromURL(testCase.input) + + if testCase.expectedScheme != resultScheme { + t.Errorf("expected scheme to be '%s' but got '%s'", testCase.expectedScheme, resultScheme) + } + + if testCase.expectedURLWithoutScheme != resultURLWithoutScheme { + t.Errorf("expected urlWithoutScheme to be '%s' but got '%s'", testCase.expectedURLWithoutScheme, resultURLWithoutScheme) + } + }) + } +} diff --git a/internal/anchore/import.go b/internal/anchore/import.go index a5c32bd38..f6f62b861 100644 --- a/internal/anchore/import.go +++ b/internal/anchore/import.go @@ -51,7 +51,7 @@ func importProgress(source string) (*progress.Stage, *progress.Manual) { // nolint:funlen func (c *Client) Import(ctx context.Context, cfg ImportConfig) error { - stage, prog := importProgress(c.config.Hostname) + stage, prog := importProgress(c.config.BaseURL) ctxWithTimeout, cancel := context.WithTimeout(ctx, time.Second*30) defer cancel()