Skip to content

Commit

Permalink
fixup! Add apt credentials parser
Browse files Browse the repository at this point in the history
  • Loading branch information
woky committed Aug 8, 2023
1 parent 482ff15 commit db609e1
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 53 deletions.
65 changes: 34 additions & 31 deletions internal/archive/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -72,17 +73,20 @@ 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
}
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.
Expand All @@ -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()
Expand All @@ -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() {
Expand All @@ -134,28 +130,29 @@ 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)

for _, file := range confFiles {
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 {
Expand All @@ -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.
//
Expand Down Expand Up @@ -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)
Expand Down
45 changes: 23 additions & 22 deletions internal/archive/credentials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:[email protected]/", "", "user", "pass"},
},
}, {
Expand All @@ -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",
Expand All @@ -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:[email protected]/foo", "", "foo", "bar"},
},
}, {
Expand All @@ -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:[email protected]/foo", "", "foo", "bar"},
},
}, {
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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"},
},
}, {
Expand Down Expand Up @@ -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",
Expand All @@ -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", ""},
},
}}

Expand Down Expand Up @@ -242,37 +242,38 @@ 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)
c.Assert(err, IsNil)

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")
}

0 comments on commit db609e1

Please sign in to comment.