Repository: amacneil/dbmate (go)

Issue:
Credentials are leaked to stderr when database URL is malformed
Let's say I give Dbmate a malformed database URL. In this case, it has a base64-encoded password which I forgot to properly URL-encode.
```bash
export DATABASE_URL=mysql://username:otS33+tb/e4=@localhost:3306/database
```
If I run `dbmate up`, I get the following output to stderr:
```
Error: parse "mysql://username:otS33+tb/e4=@localhost:3306/database": invalid port ":otS33+tb" after host
```
This leaks the database credentials to stderr, which could be problematic if stderr is being logged somewhere. I ran into this problem when trying to run Dbmate in a serverless function, which is a reasonable use-case for accessing a database which might be locked behind a VPC.

Do you think it's reasonable to mask the database credentials in the output?

Pull request description:
Redact passwords in error messages
Fixes #144

Patch:
diff --git a/main.go b/main.go
index 8452277..99909f7 100644
--- a/main.go
+++ b/main.go
@@ -5,6 +5,7 @@ import (
 	"log"
 	"net/url"
 	"os"
+	"regexp"
 
 	"github.com/joho/godotenv"
 	"github.com/urfave/cli"
@@ -19,7 +20,8 @@ func main() {
 	err := app.Run(os.Args)
 
 	if err != nil {
-		_, _ = fmt.Fprintf(os.Stderr, "Error: %s\n", err)
+		errText := redactLogString(fmt.Sprintf("Error: %s\n", err))
+		_, _ = fmt.Fprint(os.Stderr, errText)
 		os.Exit(2)
 	}
 }
@@ -219,3 +221,10 @@ func getDatabaseURL(c *cli.Context) (u *url.URL, err error) {
 
 	return url.Parse(value)
 }
+
+// redactLogString attempts to redact passwords from errors
+func redactLogString(in string) string {
+	re := regexp.MustCompile("([a-zA-Z]+://[^:]+:)[^@]+@")
+
+	return re.ReplaceAllString(in, "${1}********@")
+}


Test patch:
diff --git a/main_test.go b/main_test.go
index 1d20d06..d6922db 100644
--- a/main_test.go
+++ b/main_test.go
@@ -35,3 +35,23 @@ func TestGetDatabaseUrl(t *testing.T) {
 	require.Equal(t, "example.org", u.Host)
 	require.Equal(t, "/db", u.Path)
 }
+
+func TestRedactLogString(t *testing.T) {
+	examples := []struct {
+		in       string
+		expected string
+	}{
+		{"normal string",
+			"normal string"},
+		// malformed URL example (note forward slash in password)
+		{"parse \"mysql://username:otS33+tb/e4=@localhost:3306/database\": invalid",
+			"parse \"mysql://username:********@localhost:3306/database\": invalid"},
+		// invalid port, but probably not a password since there is no @
+		{"parse \"mysql://localhost:abc/database\": invalid",
+			"parse \"mysql://localhost:abc/database\": invalid"},
+	}
+
+	for _, ex := range examples {
+		require.Equal(t, ex.expected, redactLogString(ex.in))
+	}
+}