diff --git a/carton/init_test.go b/carton/init_test.go index fdbc65d..6c245ef 100644 --- a/carton/init_test.go +++ b/carton/init_test.go @@ -28,6 +28,7 @@ func TestUnit(t *testing.T) { suite("BuildpackDependency", testBuildpackDependency) suite("BuildImageDependency", testBuildImageDependency) suite("LifecycleDependency", testLifecycleDependency) + suite("Netrc", testNetrc) suite("Package", testPackage) suite("PackageDependency", testPackageDependency) suite.Run(t) diff --git a/carton/netrc.go b/carton/netrc.go new file mode 100644 index 0000000..f05ab86 --- /dev/null +++ b/carton/netrc.go @@ -0,0 +1,118 @@ +/* + * Copyright 2018-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package carton + +import ( + "fmt" + "io/ioutil" + "net/http" + "os" + "os/user" + "path/filepath" + "strings" +) + +type Netrc []NetrcLine + +type NetrcLine struct { + Machine string + Login string + Password string +} + +func (n Netrc) BasicAuth(request *http.Request) (*http.Request, error) { + for _, l := range n { + if l.Machine != request.Host && l.Machine != "default" { + continue + } + + request.SetBasicAuth(l.Login, l.Password) + break + } + + return request, nil +} + +func ParseNetrc(path string) (Netrc, error) { + b, err := ioutil.ReadFile(path) + if os.IsNotExist(err) { + return nil, nil + } else if err != nil { + return nil, fmt.Errorf("unable to open %s\n%w", path, err) + } + + var ( + n Netrc + l NetrcLine + m = false + ) + + for _, line := range strings.Split(string(b), "\n") { + if m { + if line == "" { + m = false + } + continue + } + + f := strings.Fields(line) + for i := 0; i < len(f); { + switch f[i] { + case "machine": + l = NetrcLine{Machine: f[i+1]} + i += 2 + case "default": + l = NetrcLine{Machine: "default"} + i += 1 + case "login": + l.Login = f[i+1] + i += 2 + case "password": + l.Password = f[i+1] + i += 2 + case "macdef": + m = true + i += 2 + } + + if l.Machine != "" && l.Login != "" && l.Password != "" { + n = append(n, l) + + if l.Machine == "default" { + return n, nil + } + + l = NetrcLine{} + } + } + } + + return n, nil +} + +func NetrcPath() (string, error) { + if s, ok := os.LookupEnv("NETRC"); ok { + return s, nil + } + + u, err := user.Current() + if err != nil { + return "", fmt.Errorf("unable to determine user home directory\n%w", err) + } + + return filepath.Join(u.HomeDir, ".netrc"), nil +} diff --git a/carton/netrc_test.go b/carton/netrc_test.go new file mode 100644 index 0000000..9f068c1 --- /dev/null +++ b/carton/netrc_test.go @@ -0,0 +1,218 @@ +/* + * Copyright 2018-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package carton_test + +import ( + "io/ioutil" + "net/http" + "os" + "os/user" + "path/filepath" + "testing" + + . "github.com/onsi/gomega" + "github.com/sclevine/spec" + + "github.com/paketo-buildpacks/libpak/carton" +) + +func testNetrc(t *testing.T, context spec.G, it spec.S) { + var ( + Expect = NewWithT(t).Expect + + path string + ) + + it.Before(func() { + var err error + + f, err := ioutil.TempFile("", "netrc") + Expect(err).NotTo(HaveOccurred()) + Expect(f.Close()).To(Succeed()) + path = f.Name() + }) + + it.After(func() { + Expect(os.RemoveAll(path)).To(Succeed()) + }) + + context("path", func() { + context("$NETRC", func() { + it.Before(func() { + Expect(os.Setenv("NETRC", "test-value")).To(Succeed()) + }) + + it.After(func() { + Expect(os.Unsetenv("NETRC")).To(Succeed()) + }) + + it("returns value from env var", func() { + Expect(carton.NetrcPath()).To(Equal("test-value")) + }) + }) + + it("returns default", func() { + u, err := user.Current() + Expect(err).NotTo(HaveOccurred()) + + Expect(carton.NetrcPath()).To(Equal(filepath.Join(u.HomeDir, ".netrc"))) + }) + }) + + context("parse", func() { + it("parses one-liner", func() { + Expect(ioutil.WriteFile(path, []byte(`machine test-machine login test-login password test-password`), 0644)).To(Succeed()) + + Expect(carton.ParseNetrc(path)).To(Equal(carton.Netrc{ + { + Machine: "test-machine", + Login: "test-login", + Password: "test-password", + }, + })) + }) + + it("parses multi-liner", func() { + Expect(ioutil.WriteFile(path, []byte(` +machine test-machine +login test-login +password test-password +`), 0644)).To(Succeed()) + + Expect(carton.ParseNetrc(path)).To(Equal(carton.Netrc{ + { + Machine: "test-machine", + Login: "test-login", + Password: "test-password", + }, + })) + }) + + it("ignores macdef", func() { + Expect(ioutil.WriteFile(path, []byte(` +macdef uploadtest + cd /pub/tests + bin + put filename.tar.gz + quit + +machine test-machine login test-login password test-password +`), 0644)).To(Succeed()) + + Expect(carton.ParseNetrc(path)).To(Equal(carton.Netrc{ + { + Machine: "test-machine", + Login: "test-login", + Password: "test-password", + }, + })) + }) + + it("ignores all after default", func() { + Expect(ioutil.WriteFile(path, []byte(` +machine test-machine-1 login test-login-1 password test-password-1 + +default +login test-login-2 +password test-password-2 + +machine test-machine-3 login test-login-3 password test-password-3 +`), 0644)).To(Succeed()) + + Expect(carton.ParseNetrc(path)).To(Equal(carton.Netrc{ + { + Machine: "test-machine-1", + Login: "test-login-1", + Password: "test-password-1", + }, + { + Machine: "default", + Login: "test-login-2", + Password: "test-password-2", + }, + })) + }) + }) + + context("basic auth", func() { + it("does not apply auth if no candidates", func() { + n := carton.Netrc{ + { + Machine: "test-machine", + Login: "test-login", + Password: "test-password", + }, + } + + req, err := http.NewRequest("GET", "http://another-machine", nil) + Expect(err).NotTo(HaveOccurred()) + + req, err = n.BasicAuth(req) + Expect(err).NotTo(HaveOccurred()) + + _, _, ok := req.BasicAuth() + Expect(ok).To(BeFalse()) + }) + + it("applies basic auth for match", func() { + n := carton.Netrc{ + { + Machine: "test-machine", + Login: "test-login", + Password: "test-password", + }, + } + + req, err := http.NewRequest("GET", "http://test-machine", nil) + Expect(err).NotTo(HaveOccurred()) + + req, err = n.BasicAuth(req) + Expect(err).NotTo(HaveOccurred()) + + u, p, ok := req.BasicAuth() + Expect(ok).To(BeTrue()) + Expect(u).To(Equal("test-login")) + Expect(p).To(Equal("test-password")) + }) + + it("applies basic auth for default", func() { + n := carton.Netrc{ + { + Machine: "test-machine", + Login: "test-login", + Password: "test-password", + }, + { + Machine: "default", + Login: "default-login", + Password: "default-password", + }, + } + + req, err := http.NewRequest("GET", "http://another-machine", nil) + Expect(err).NotTo(HaveOccurred()) + + req, err = n.BasicAuth(req) + Expect(err).NotTo(HaveOccurred()) + + u, p, ok := req.BasicAuth() + Expect(ok).To(BeTrue()) + Expect(u).To(Equal("default-login")) + Expect(p).To(Equal("default-password")) + }) + }) +} diff --git a/carton/package.go b/carton/package.go index 2cdcd35..a350932 100644 --- a/carton/package.go +++ b/carton/package.go @@ -152,10 +152,22 @@ func (p Package) Create(options ...Option) { cache.DownloadPath = filepath.Join(p.Source, "dependencies") } + np, err := NetrcPath() + if err != nil { + config.exitHandler.Error(fmt.Errorf("unable to determine netrc path\n%w", err)) + return + } + + n, err := ParseNetrc(np) + if err != nil { + config.exitHandler.Error(fmt.Errorf("unable to read %s as netrc\n%w", np, err)) + return + } + for _, dep := range metadata.Dependencies { logger.Headerf("Caching %s", color.BlueString("%s %s", dep.Name, dep.Version)) - f, err := cache.Artifact(dep) + f, err := cache.Artifact(dep, n.BasicAuth) if err != nil { config.exitHandler.Error(fmt.Errorf("unable to download %s\n%w", dep.URI, err)) return diff --git a/dependency_cache.go b/dependency_cache.go index 75cc861..624f510 100644 --- a/dependency_cache.go +++ b/dependency_cache.go @@ -99,19 +99,7 @@ type RequestModifierFunc func(request *http.Request) (*http.Request, error) // // If the BuildpackDependency's SHA256 is not set, the download can never be verified to be up to date and will always // download, skipping all the caches. -func (d *DependencyCache) Artifact(dependency BuildpackDependency) (*os.File, error) { - return d.ArtifactWithRequestModification(dependency, nil) -} - -// ArtifactWithRequestModification returns the path to the artifact. Resolution of that path follows three tiers: -// -// 1. CachePath -// 2. DownloadPath -// 3. Download from URI -// -// If the BuildpackDependency's SHA256 is not set, the download can never be verified to be up to date and will always -// download, skipping all the caches. -func (d *DependencyCache) ArtifactWithRequestModification(dependency BuildpackDependency, f RequestModifierFunc) (*os.File, error) { +func (d *DependencyCache) Artifact(dependency BuildpackDependency, mods ...RequestModifierFunc) (*os.File, error) { var ( actual BuildpackDependency @@ -133,7 +121,7 @@ func (d *DependencyCache) ArtifactWithRequestModification(dependency BuildpackDe d.Logger.Bodyf("%s from %s", color.YellowString("Downloading"), uri) artifact = filepath.Join(d.DownloadPath, filepath.Base(uri)) - if err := d.download(uri, artifact, f); err != nil { + if err := d.download(uri, artifact, mods...); err != nil { return nil, fmt.Errorf("unable to download %s\n%w", uri, err) } @@ -162,7 +150,7 @@ func (d *DependencyCache) ArtifactWithRequestModification(dependency BuildpackDe d.Logger.Bodyf("%s from %s", color.YellowString("Downloading"), uri) artifact = filepath.Join(d.DownloadPath, dependency.SHA256, filepath.Base(uri)) - if err := d.download(uri, artifact, f); err != nil { + if err := d.download(uri, artifact, mods...); err != nil { return nil, fmt.Errorf("unable to download %s\n%w", uri, err) } @@ -189,7 +177,7 @@ func (d *DependencyCache) ArtifactWithRequestModification(dependency BuildpackDe return os.Open(artifact) } -func (d DependencyCache) download(uri string, destination string, f RequestModifierFunc) error { +func (d DependencyCache) download(uri string, destination string, mods ...RequestModifierFunc) error { req, err := http.NewRequest("GET", uri, nil) if err != nil { return fmt.Errorf("unable to create new GET request for %s\n%w", uri, err) @@ -199,8 +187,8 @@ func (d DependencyCache) download(uri string, destination string, f RequestModif req.Header.Set("User-Agent", d.UserAgent) } - if f != nil { - req, err = f(req) + for _, m := range mods { + req, err = m(req) if err != nil { return fmt.Errorf("unable to modify request\n%w", err) } diff --git a/dependency_cache_test.go b/dependency_cache_test.go index bc8d157..81321ec 100644 --- a/dependency_cache_test.go +++ b/dependency_cache_test.go @@ -284,7 +284,7 @@ func testDependencyCache(t *testing.T, context spec.G, it spec.S) { ghttp.RespondWith(http.StatusOK, "test-fixture"), )) - a, err := dependencyCache.ArtifactWithRequestModification(dependency, func(request *http.Request) (*http.Request, error) { + a, err := dependencyCache.Artifact(dependency, func(request *http.Request) (*http.Request, error) { request.Header.Add("Test-Key", "test-value") return request, nil }) diff --git a/layer.go b/layer.go index fffa0b1..9a31cb9 100644 --- a/layer.go +++ b/layer.go @@ -134,8 +134,8 @@ type DependencyLayerContributor struct { // Logger is the logger to use. Logger bard.Logger - // RequestModifierFunc is an optional Request Modifier to use when downloading the dependency. - RequestModifierFunc RequestModifierFunc + // RequestModifierFuncs is an optional Request Modifier to use when downloading the dependency. + RequestModifierFuncs []RequestModifierFunc } // NewDependencyLayerContributor creates a new instance and adds the dependency to the Buildpack Plan. @@ -161,7 +161,7 @@ func (d *DependencyLayerContributor) Contribute(layer libcnb.Layer, f Dependency d.LayerContributor.Logger = d.Logger return d.LayerContributor.Contribute(layer, func() (libcnb.Layer, error) { - artifact, err := d.DependencyCache.ArtifactWithRequestModification(d.Dependency, d.RequestModifierFunc) + artifact, err := d.DependencyCache.Artifact(d.Dependency, d.RequestModifierFuncs...) if err != nil { return libcnb.Layer{}, fmt.Errorf("unable to get dependency %s\n%w", d.Dependency.ID, err) } diff --git a/layer_test.go b/layer_test.go index cc29270..f1c92eb 100644 --- a/layer_test.go +++ b/layer_test.go @@ -226,10 +226,10 @@ func testLayer(t *testing.T, context spec.G, it spec.S) { ghttp.RespondWith(http.StatusOK, "test-fixture"), )) - dlc.RequestModifierFunc = func(request *http.Request) (*http.Request, error) { + dlc.RequestModifierFuncs = append(dlc.RequestModifierFuncs, func(request *http.Request) (*http.Request, error) { request.Header.Add("Test-Key", "test-value") return request, nil - } + }) _, err := dlc.Contribute(layer, func(artifact *os.File) (libcnb.Layer, error) { defer artifact.Close()