package ui import ( "fmt" "net" "net/url" "strings" ) // ConnKind identifies the database type a connection string targets. type ConnKind string const ( ConnPostgres ConnKind = "postgres" ConnMSSQL ConnKind = "mssql" ConnSQLite ConnKind = "sqlite" ) // connKinds lists the kinds offered by the builder dialog, in display order. var connKinds = []ConnKind{ConnPostgres, ConnMSSQL, ConnSQLite} // maskedPassword is substituted for the password in previews. const maskedPassword = "****" // ConnFields holds the editable parts of a connection string. type ConnFields struct { Kind ConnKind Host string Port string Database string User string Password string SSLMode string FilePath string // SQLite only // Extra keeps query parameters the builder has no field for, so that // parsing and rebuilding an existing string does not drop them. Extra url.Values } // DefaultConnFields returns sensible defaults for the given kind. func DefaultConnFields(kind ConnKind) ConnFields { f := ConnFields{Kind: kind} switch kind { case ConnPostgres: f.Host, f.Port, f.User, f.SSLMode = "localhost", "5432", "postgres", "disable" case ConnMSSQL: f.Host, f.Port, f.User, f.SSLMode = "localhost", "1433", "sa", "disable" } return f } // SSLModes returns the valid SSL/encryption options for a kind. func SSLModes(kind ConnKind) []string { switch kind { case ConnPostgres: return []string{"disable", "allow", "prefer", "require", "verify-ca", "verify-full"} case ConnMSSQL: return []string{"disable", "false", "true"} } return nil } func (f ConnFields) sslParam() string { if f.Kind == ConnMSSQL { return "encrypt" } return "sslmode" } // BuildConnString renders the fields as a connection string. With mask set, // a non-empty password is replaced by asterisks (for previews). func BuildConnString(f ConnFields, mask bool) string { if f.Kind == ConnSQLite { return f.FilePath } u := &url.URL{Scheme: "postgres"} if f.Kind == ConnMSSQL { u.Scheme = "sqlserver" } if f.Port != "" { u.Host = net.JoinHostPort(f.Host, f.Port) } else { u.Host = f.Host } if f.User != "" { if f.Password != "" { pw := f.Password if mask { pw = maskedPassword } u.User = url.UserPassword(f.User, pw) } else { u.User = url.User(f.User) } } query := url.Values{} for k, v := range f.Extra { query[k] = v } if f.Kind == ConnMSSQL { if f.Database != "" { query.Set("database", f.Database) } } else if f.Database != "" { u.Path = "/" + f.Database } if f.SSLMode != "" { query.Set(f.sslParam(), f.SSLMode) } u.RawQuery = query.Encode() out := u.String() if mask { // url escapes '*' in the userinfo; keep the preview readable. out = strings.Replace(out, url.QueryEscape(maskedPassword), maskedPassword, 1) } return out } // DetectConnKind guesses the kind from a connection string's scheme. Anything // that is not a recognised URL is treated as a SQLite file path. func DetectConnKind(s string) ConnKind { lower := strings.ToLower(strings.TrimSpace(s)) switch { case strings.HasPrefix(lower, "postgres://"), strings.HasPrefix(lower, "postgresql://"): return ConnPostgres case strings.HasPrefix(lower, "sqlserver://"), strings.HasPrefix(lower, "mssql://"): return ConnMSSQL } return ConnSQLite } // ParseConnString splits a connection string into fields. An empty string // yields the defaults for hint. Missing ports fall back to the kind default. func ParseConnString(s string, hint ConnKind) (ConnFields, error) { s = strings.TrimSpace(s) if s == "" { return DefaultConnFields(hint), nil } kind := DetectConnKind(s) if kind == ConnSQLite { path := s for _, prefix := range []string{"sqlite://", "sqlite3://"} { path = strings.TrimPrefix(path, prefix) } return ConnFields{Kind: ConnSQLite, FilePath: path}, nil } u, err := url.Parse(s) if err != nil { return DefaultConnFields(kind), fmt.Errorf("invalid connection string: %w", err) } f := ConnFields{ Kind: kind, Host: u.Hostname(), Port: u.Port(), } if f.Port == "" { f.Port = DefaultConnFields(kind).Port } if u.User != nil { f.User = u.User.Username() f.Password, _ = u.User.Password() } query := u.Query() if kind == ConnMSSQL { f.Database = query.Get("database") query.Del("database") } else { f.Database = strings.TrimPrefix(u.Path, "/") } f.SSLMode = query.Get(f.sslParam()) query.Del(f.sslParam()) if len(query) > 0 { f.Extra = query } return f, nil }