From db609e137322af123e9e9dc09920bee7648a579b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Virtus?= Date: Tue, 8 Aug 2023 14:13:22 +0200 Subject: [PATCH] fixup! Add apt credentials parser --- internal/archive/credentials.go | 65 +++++++++++++++------------- internal/archive/credentials_test.go | 45 +++++++++---------- 2 files changed, 57 insertions(+), 53 deletions(-) diff --git a/internal/archive/credentials.go b/internal/archive/credentials.go index a0373be3..8a91476a 100644 --- a/internal/archive/credentials.go +++ b/internal/archive/credentials.go @@ -35,12 +35,13 @@ type credentialsQuery struct { // parseRepoURL parses repoURL into credentialsQuery and fills provided // credentials with username and password if they are specified in repoURL. -func parseRepoURL(repoURL string) (creds credentials, query *credentialsQuery, err error) { +func parseRepoURL(repoURL string) (creds *credentials, query *credentialsQuery, err error) { u, err := url.Parse(repoURL) if err != nil { return } + creds = &credentials{} creds.Username = u.User.Username() creds.Password, _ = u.User.Password() @@ -72,10 +73,12 @@ func parseRepoURL(repoURL string) (creds credentials, query *credentialsQuery, e return } +var ErrCredentialsNotFound = errors.New("credentials not found") + // findCredentials searches credentials for repoURL in configuration files in // directory specified by CHISEL_AUTH_DIR environment variable if it's // non-empty, otherwise /etc/apt/auth.conf.d. -func findCredentials(repoURL string) (credentials, error) { +func findCredentials(repoURL string) (*credentials, error) { credsDir := "/etc/apt/auth.conf.d" if v := os.Getenv("CHISEL_AUTH_DIR"); v != "" { credsDir = v @@ -83,6 +86,7 @@ func findCredentials(repoURL string) (credentials, error) { return findCredentialsInDir(repoURL, credsDir) } +var FindCredentials = findCredentials // findCredentialsInDir searches for credentials for repoURL in configuration // files in credsDir directory. If the directory does not exist, empty // credentials structure with nil err is returned. @@ -91,29 +95,21 @@ func findCredentials(repoURL string) (credentials, error) { // order. The first file that contains machine declaration matching repoURL // ends the search. If no file contain matching machine declaration, empty // credentials structure with nil err is returned. -func findCredentialsInDir(repoURL string, credsDir string) (creds credentials, err error) { +func findCredentialsInDir(repoURL string, credsDir string) (*credentials, error) { contents, err := os.ReadDir(credsDir) if err != nil { - if os.IsNotExist(err) { - err = nil - debugf("credentials directory %#v does not exist", credsDir) - } else { - err = fmt.Errorf("cannot open credentials directory: %w", err) - } - return + logf("Cannot open credentials directory %q: %v", credsDir, err) + return nil, ErrCredentialsNotFound } creds, query, err := parseRepoURL(repoURL) if err != nil { - err = fmt.Errorf("cannot parse archive URL: %w", err) - return + return nil, fmt.Errorf("cannot parse archive URL: %v", err) } - if query == nil { // creds.Empty() == false - return + if !creds.Empty() { + return creds, nil } - errs := make([]error, 0) - confFiles := make([]string, 0, len(contents)) for _, entry := range contents { name := entry.Name() @@ -125,7 +121,7 @@ func findCredentialsInDir(repoURL string, credsDir string) (creds credentials, e } info, err := entry.Info() if err != nil { - errs = append(errs, fmt.Errorf("cannot stat credentials file: %w", err)) + logf("Cannot stat credentials file %q: %v", filepath.Join(credsDir, name), err) continue } if !info.Mode().IsRegular() { @@ -134,8 +130,7 @@ func findCredentialsInDir(repoURL string, credsDir string) (creds credentials, e confFiles = append(confFiles, name) } if len(confFiles) == 0 { - err = errors.Join(errs...) - return + return nil, ErrCredentialsNotFound } sort.Strings(confFiles) @@ -143,19 +138,21 @@ func findCredentialsInDir(repoURL string, credsDir string) (creds credentials, e fpath := filepath.Join(credsDir, file) f, err := os.Open(fpath) if err != nil { - errs = append(errs, fmt.Errorf("cannot read credentials file %s: %w", fpath, err)) + logf("Cannot open credentials file %q: %v", fpath, err) continue } - - if err = findCredsInFile(query, f, &creds); err != nil { - errs = append(errs, fmt.Errorf("cannot parse credentials file %s: %w", fpath, err)) - } else if !creds.Empty() { - break + creds, err = findCredentialsInternal(query, f) + if closeErr := f.Close(); closeErr != nil { + logf("Cannot close credentials file %q: %v", fpath, err) + } + if err == nil { + return creds, nil + } else if err != ErrCredentialsNotFound { + logf("Cannot parse credentials file %q: %v", fpath, err) } } - err = errors.Join(errs...) - return + return nil, ErrCredentialsNotFound } type netrcParser struct { @@ -164,7 +161,7 @@ type netrcParser struct { creds *credentials } -// findCredsInFile searches for credentials in netrc file matching query +// findCredentialsInternal searches for credentials in netrc file matching query // and fills creds with matched credentials if there's a match. The first match // ends the search. // @@ -207,20 +204,26 @@ type netrcParser struct { // [3] https://salsa.debian.org/apt-team/apt/-/blob/4e04cbaf/methods/aptmethod.h#L560 // [4] https://www.gnu.org/software/inetutils/manual/html_node/The-_002enetrc-file.html // [5] https://daniel.haxx.se/blog/2022/05/31/netrc-pains/ -func findCredsInFile(query *credentialsQuery, netrc io.Reader, creds *credentials) error { +func findCredentialsInternal(query *credentialsQuery, netrc io.Reader) (*credentials, error) { s := bufio.NewScanner(netrc) s.Split(bufio.ScanWords) p := netrcParser{ query: query, scanner: s, - creds: creds, + creds: &credentials{}, } var err error for state := netrcStart; err == nil && state != nil; { state, err = state(&p) err = errors.Join(err, p.scanner.Err()) } - return err + if err != nil { + return nil, err + } + if p.creds.Empty() { + return nil, ErrCredentialsNotFound + } + return p.creds, nil } type netrcState func(*netrcParser) (netrcState, error) diff --git a/internal/archive/credentials_test.go b/internal/archive/credentials_test.go index 34dfa4d5..6f2802f3 100644 --- a/internal/archive/credentials_test.go +++ b/internal/archive/credentials_test.go @@ -54,14 +54,14 @@ machine socks5h://example.last/debian login debian password rules {"https://example.org:8080/foo", "", "example", "foobar"}, {"https://example.net:42/foo", "", "foo", "bar"}, {"https://example.org/foo", "", "anonymous", "pass"}, - {"https://example.com/apt", "", "", ""}, + {"https://example.com/apt", "^credentials not found$", "", ""}, {"https://example.com/foo", "", "user1", "pass1"}, {"https://example.com/fooo", "", "user1", "pass1"}, - {"https://example.com/fo", "", "", ""}, + {"https://example.com/fo", "^credentials not found$", "", ""}, {"https://example.com/bar", "", "user2", "pass2"}, {"https://example.com/user", "", "user", ""}, {"socks5h://example.last/debian", "", "debian", "rules"}, - {"socks5h://example.debian/", "", "", ""}, + {"socks5h://example.debian/", "^credentials not found$", "", ""}, {"socks5h://user:pass@example.debian/", "", "user", "pass"}, }, }, { @@ -74,7 +74,7 @@ machine2 example.org login foo3 password bar `, }, matchTests: []matchTest{ - {"https://example.org/foo", "", "", ""}, + {"https://example.org/foo", "^credentials not found$", "", ""}, }, }, { summary: "Bad file: Ends machine", @@ -85,7 +85,7 @@ machine`, }, matchTests: []matchTest{ {"https://example.org/foo", "", "foo1", "bar"}, - {"https://example.net/foo", ".*\\breached end of file while expecting machine text\\b.*", "", ""}, + {"https://example.net/foo", "^credentials not found$", "", ""}, {"https://foo:bar@example.net/foo", "", "foo", "bar"}, }, }, { @@ -98,7 +98,7 @@ machine example.net login }, matchTests: []matchTest{ {"https://example.org/foo", "", "foo1", "bar"}, - {"https://example.net/foo", ".*\\breached end of file while expecting username text\\b.*", "", ""}, + {"https://example.net/foo", "^credentials not found$", "", ""}, {"https://foo:bar@example.net/foo", "", "foo", "bar"}, }, }, { @@ -111,9 +111,9 @@ machine http://http.example login foo1 password bar }, matchTests: []matchTest{ {"https://https.example/foo", "", "foo1", "bar"}, - {"http://https.example/foo", "", "", ""}, + {"http://https.example/foo", "^credentials not found$", "", ""}, {"http://http.example/foo", "", "foo1", "bar"}, - {"https://http.example/foo", "", "", ""}, + {"https://http.example/foo", "^credentials not found$", "", ""}, }, }, { summary: "Password is machine", @@ -125,7 +125,7 @@ machine http://site2.com login u2 password p2 }, matchTests: []matchTest{ {"http://site1.com/foo", "", "u1", "machine"}, - {"http://site2.com/bar", "", "", ""}, + {"http://site2.com/bar", "^credentials not found$", "", ""}, }, }, { summary: "Multiple login and password tokens", @@ -143,8 +143,8 @@ machine http://site2.com login f password g summary: "Empty auth dir", credsFiles: map[string]string{}, matchTests: []matchTest{ - {"https://example.com/foo", "", "", ""}, - {"http://zombo.com", "", "", ""}, + {"https://example.com/foo", "^credentials not found$", "", ""}, + {"http://zombo.com", "^credentials not found$", "", ""}, }, }, { summary: "Invalid input URL", @@ -155,7 +155,7 @@ machine login foo password bar login baz }, matchTests: []matchTest{ {":http:foo", "cannot parse archive URL: parse \":http:foo\": missing protocol scheme", "", ""}, - {"", "", "", ""}, // this is fine URL apparently, but won't ever match + {"", "^credentials not found$", "", ""}, // this is fine URL apparently, but won't ever match {"https://login", "", "baz", "bar"}, }, }, { @@ -204,7 +204,7 @@ machine http://example.com/foo login `, }, matchTests: []matchTest{ - {"http://example.com/foo", "cannot parse credentials file .*/nouser: syntax error: reached end of file while expecting username text", "", ""}, + {"http://example.com/foo", "^credentials not found$", "", ""}, }, }, { summary: "EOF while epxecting password", @@ -214,7 +214,7 @@ machine http://example.com/foo login a password `, }, matchTests: []matchTest{ - {"http://example.com/foo", "cannot parse credentials file .*/nopw: syntax error: reached end of file while expecting password text", "a", ""}, + {"http://example.com/foo", "^credentials not found$", "a", ""}, }, }} @@ -242,29 +242,30 @@ func (s *S) runFindCredentialsTest(c *C, t *credentialsTest) { c.Assert(err, ErrorMatches, matchTest.err) } else { c.Assert(err, IsNil) + c.Assert(creds, NotNil) + c.Assert(creds.Username, Equals, matchTest.username) + c.Assert(creds.Password, Equals, matchTest.password) } - c.Assert(creds.Username, Equals, matchTest.username) - c.Assert(creds.Password, Equals, matchTest.password) } } func (s *S) TestFindCredentialsMissingDir(c *C) { - var creds, emptyCreds archive.Credentials + var creds *archive.Credentials var err error workDir := c.MkDir() credsDir := filepath.Join(workDir, "auth.conf.d") creds, err = archive.FindCredentialsInDir("https://example.com/foo/bar", credsDir) - c.Assert(err, IsNil) - c.Assert(creds, Equals, emptyCreds) + c.Assert(err, ErrorMatches, "^credentials not found$") + c.Assert(creds, IsNil) err = os.Mkdir(credsDir, 0755) c.Assert(err, IsNil) creds, err = archive.FindCredentialsInDir("https://example.com/foo/bar", credsDir) - c.Assert(err, IsNil) - c.Assert(creds, Equals, emptyCreds) + c.Assert(err, ErrorMatches, "^credentials not found$") + c.Assert(creds, IsNil) confFile := filepath.Join(credsDir, "example") err = os.WriteFile(confFile, []byte("machine example.com login admin password swordfish"), 0600) @@ -272,7 +273,7 @@ func (s *S) TestFindCredentialsMissingDir(c *C) { creds, err = archive.FindCredentialsInDir("https://example.com/foo/bar", credsDir) c.Assert(err, IsNil) - c.Assert(creds, Not(Equals), emptyCreds) + c.Assert(creds, NotNil) c.Assert(creds.Username, Equals, "admin") c.Assert(creds.Password, Equals, "swordfish") }