Backport #27723 by @mpldr This patchset changes the connection string builder to use net.URL and the host/port parser to use the stdlib function for splitting host from port. It also adds a footnote about a potentially required portnumber for postgres UNIX sockets. Fixes: #24552 Co-authored-by: Moritz Poldrack <33086936+mpldr@users.noreply.github.com>tags/v1.21.0
## Database (`database`) | ## Database (`database`) | ||||
- `DB_TYPE`: **mysql**: The database type in use \[mysql, postgres, mssql, sqlite3\]. | - `DB_TYPE`: **mysql**: The database type in use \[mysql, postgres, mssql, sqlite3\]. | ||||
- `HOST`: **127.0.0.1:3306**: Database host address and port or absolute path for unix socket \[mysql, postgres\] (ex: /var/run/mysqld/mysqld.sock). | |||||
- `HOST`: **127.0.0.1:3306**: Database host address and port or absolute path for unix socket \[mysql, postgres[^1]\] (ex: /var/run/mysqld/mysqld.sock). | |||||
- `NAME`: **gitea**: Database name. | - `NAME`: **gitea**: Database name. | ||||
- `USER`: **root**: Database username. | - `USER`: **root**: Database username. | ||||
- `PASSWD`: **_empty_**: Database user password. Use \`your password\` or """your password""" for quoting if you use special characters in the password. | - `PASSWD`: **_empty_**: Database user password. Use \`your password\` or """your password""" for quoting if you use special characters in the password. | ||||
- `CONN_MAX_LIFETIME` **0 or 3s**: Sets the maximum amount of time a DB connection may be reused - default is 0, meaning there is no limit (except on MySQL where it is 3s - see #6804 & #7071). | - `CONN_MAX_LIFETIME` **0 or 3s**: Sets the maximum amount of time a DB connection may be reused - default is 0, meaning there is no limit (except on MySQL where it is 3s - see #6804 & #7071). | ||||
- `AUTO_MIGRATION` **true**: Whether execute database models migrations automatically. | - `AUTO_MIGRATION` **true**: Whether execute database models migrations automatically. | ||||
[^1]: It may be necessary to specify a hostport even when listening on a unix socket, as the port is part of the socket name. see [#24552](https://github.com/go-gitea/gitea/issues/24552#issuecomment-1681649367) for additional details. | |||||
Please see #8540 & #8273 for further discussion of the appropriate values for `MAX_OPEN_CONNS`, `MAX_IDLE_CONNS` & `CONN_MAX_LIFETIME` and their | Please see #8540 & #8273 for further discussion of the appropriate values for `MAX_OPEN_CONNS`, `MAX_IDLE_CONNS` & `CONN_MAX_LIFETIME` and their | ||||
relation to port exhaustion. | relation to port exhaustion. | ||||
import ( | import ( | ||||
"errors" | "errors" | ||||
"fmt" | "fmt" | ||||
"net" | |||||
"net/url" | "net/url" | ||||
"os" | "os" | ||||
"path" | "path" | ||||
// parsePostgreSQLHostPort parses given input in various forms defined in | // parsePostgreSQLHostPort parses given input in various forms defined in | ||||
// https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING | // https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING | ||||
// and returns proper host and port number. | // and returns proper host and port number. | ||||
func parsePostgreSQLHostPort(info string) (string, string) { | |||||
host, port := "127.0.0.1", "5432" | |||||
if strings.Contains(info, ":") && !strings.HasSuffix(info, "]") { | |||||
idx := strings.LastIndex(info, ":") | |||||
host = info[:idx] | |||||
port = info[idx+1:] | |||||
} else if len(info) > 0 { | |||||
func parsePostgreSQLHostPort(info string) (host, port string) { | |||||
if h, p, err := net.SplitHostPort(info); err == nil { | |||||
host, port = h, p | |||||
} else { | |||||
// treat the "info" as "host", if it's an IPv6 address, remove the wrapper | |||||
host = info | host = info | ||||
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { | |||||
host = host[1 : len(host)-1] | |||||
} | |||||
} | } | ||||
// set fallback values | |||||
if host == "" { | if host == "" { | ||||
host = "127.0.0.1" | host = "127.0.0.1" | ||||
} | } | ||||
func getPostgreSQLConnectionString(dbHost, dbUser, dbPasswd, dbName, dbParam, dbsslMode string) (connStr string) { | func getPostgreSQLConnectionString(dbHost, dbUser, dbPasswd, dbName, dbParam, dbsslMode string) (connStr string) { | ||||
host, port := parsePostgreSQLHostPort(dbHost) | host, port := parsePostgreSQLHostPort(dbHost) | ||||
if host[0] == '/' { // looks like a unix socket | |||||
connStr = fmt.Sprintf("postgres://%s:%s@:%s/%s%ssslmode=%s&host=%s", | |||||
url.PathEscape(dbUser), url.PathEscape(dbPasswd), port, dbName, dbParam, dbsslMode, host) | |||||
} else { | |||||
connStr = fmt.Sprintf("postgres://%s:%s@%s:%s/%s%ssslmode=%s", | |||||
url.PathEscape(dbUser), url.PathEscape(dbPasswd), host, port, dbName, dbParam, dbsslMode) | |||||
connURL := url.URL{ | |||||
Scheme: "postgres", | |||||
User: url.UserPassword(dbUser, dbPasswd), | |||||
Host: net.JoinHostPort(host, port), | |||||
Path: dbName, | |||||
OmitHost: false, | |||||
RawQuery: dbParam, | |||||
} | |||||
query := connURL.Query() | |||||
if dbHost[0] == '/' { // looks like a unix socket | |||||
query.Add("host", dbHost) | |||||
connURL.Host = ":" + port | |||||
} | } | ||||
return connStr | |||||
query.Set("sslmode", dbsslMode) | |||||
connURL.RawQuery = query.Encode() | |||||
return connURL.String() | |||||
} | } | ||||
// ParseMSSQLHostPort splits the host into host and port | // ParseMSSQLHostPort splits the host into host and port |
) | ) | ||||
func Test_parsePostgreSQLHostPort(t *testing.T) { | func Test_parsePostgreSQLHostPort(t *testing.T) { | ||||
tests := []struct { | |||||
tests := map[string]struct { | |||||
HostPort string | HostPort string | ||||
Host string | Host string | ||||
Port string | Port string | ||||
}{ | }{ | ||||
{ | |||||
"host-port": { | |||||
HostPort: "127.0.0.1:1234", | HostPort: "127.0.0.1:1234", | ||||
Host: "127.0.0.1", | Host: "127.0.0.1", | ||||
Port: "1234", | Port: "1234", | ||||
}, | }, | ||||
{ | |||||
"no-port": { | |||||
HostPort: "127.0.0.1", | HostPort: "127.0.0.1", | ||||
Host: "127.0.0.1", | Host: "127.0.0.1", | ||||
Port: "5432", | Port: "5432", | ||||
}, | }, | ||||
{ | |||||
"ipv6-port": { | |||||
HostPort: "[::1]:1234", | HostPort: "[::1]:1234", | ||||
Host: "[::1]", | |||||
Host: "::1", | |||||
Port: "1234", | Port: "1234", | ||||
}, | }, | ||||
{ | |||||
"ipv6-no-port": { | |||||
HostPort: "[::1]", | HostPort: "[::1]", | ||||
Host: "[::1]", | |||||
Host: "::1", | |||||
Port: "5432", | Port: "5432", | ||||
}, | }, | ||||
{ | |||||
"unix-socket": { | |||||
HostPort: "/tmp/pg.sock:1234", | HostPort: "/tmp/pg.sock:1234", | ||||
Host: "/tmp/pg.sock", | Host: "/tmp/pg.sock", | ||||
Port: "1234", | Port: "1234", | ||||
}, | }, | ||||
{ | |||||
"unix-socket-no-port": { | |||||
HostPort: "/tmp/pg.sock", | HostPort: "/tmp/pg.sock", | ||||
Host: "/tmp/pg.sock", | Host: "/tmp/pg.sock", | ||||
Port: "5432", | Port: "5432", | ||||
}, | }, | ||||
} | } | ||||
for _, test := range tests { | |||||
host, port := parsePostgreSQLHostPort(test.HostPort) | |||||
assert.Equal(t, test.Host, host) | |||||
assert.Equal(t, test.Port, port) | |||||
for k, test := range tests { | |||||
t.Run(k, func(t *testing.T) { | |||||
t.Log(test.HostPort) | |||||
host, port := parsePostgreSQLHostPort(test.HostPort) | |||||
assert.Equal(t, test.Host, host) | |||||
assert.Equal(t, test.Port, port) | |||||
}) | |||||
} | } | ||||
} | } | ||||
Name: "gitea", | Name: "gitea", | ||||
Param: "", | Param: "", | ||||
SSLMode: "false", | SSLMode: "false", | ||||
Output: "postgres://testuser:space%20space%20%21%23$%25%5E%5E%25%5E%60%60%60-=%3F=@:5432/giteasslmode=false&host=/tmp/pg.sock", | |||||
Output: "postgres://testuser:space%20space%20%21%23$%25%5E%5E%25%5E%60%60%60-=%3F=@:5432/gitea?host=%2Ftmp%2Fpg.sock&sslmode=false", | |||||
}, | }, | ||||
{ | { | ||||
Host: "localhost", | Host: "localhost", | ||||
Name: "gitea", | Name: "gitea", | ||||
Param: "", | Param: "", | ||||
SSLMode: "true", | SSLMode: "true", | ||||
Output: "postgres://pgsqlusername:I%20love%20Gitea%21@localhost:5432/giteasslmode=true", | |||||
Output: "postgres://pgsqlusername:I%20love%20Gitea%21@localhost:5432/gitea?sslmode=true", | |||||
}, | }, | ||||
} | } | ||||