diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index 8fa0515c6a0..1050db5c983 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -2248,6 +2248,19 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname + + servicefile + + + This option specifies the name of the per-user connection service file + (see ). + Defaults to ~/.pg_service.conf, or + %APPDATA%\postgresql\.pg_service.conf on + Microsoft Windows. + + + + target_session_attrs @@ -9504,7 +9517,8 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough) On Microsoft Windows, it is named %APPDATA%\postgresql\.pg_service.conf (where %APPDATA% refers to the Application Data subdirectory - in the user's profile). A different file name can be specified by + in the user's profile). A different file name can be specified using the + servicefile key word in a libpq connection string or by setting the environment variable PGSERVICEFILE. The system-wide file is named pg_service.conf. By default it is sought in the etc directory diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index d5051f5e820..f16468be7d1 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -195,6 +195,9 @@ static const internalPQconninfoOption PQconninfoOptions[] = { "Database-Service", "", 20, offsetof(struct pg_conn, pgservice)}, + {"servicefile", "PGSERVICEFILE", NULL, NULL, + "Database-Service-File", "", 64, -1}, + {"user", "PGUSER", NULL, NULL, "Database-User", "", 20, offsetof(struct pg_conn, pguser)}, @@ -5838,6 +5841,7 @@ static int parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage) { const char *service = conninfo_getval(options, "service"); + const char *service_fname = conninfo_getval(options, "servicefile"); char serviceFile[MAXPGPATH]; char *env; bool group_found = false; @@ -5857,11 +5861,18 @@ parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage) return 0; /* - * Try PGSERVICEFILE if specified, else try ~/.pg_service.conf (if that - * exists). + * First, check servicefile option on connection string. Second, check + * PGSERVICEFILE environment variable. Finally, check ~/.pg_service.conf + * (if that exists). */ - if ((env = getenv("PGSERVICEFILE")) != NULL) + if (service_fname != NULL) + { + strlcpy(serviceFile, service_fname, sizeof(serviceFile)); + } + else if ((env = getenv("PGSERVICEFILE")) != NULL) + { strlcpy(serviceFile, env, sizeof(serviceFile)); + } else { char homedir[MAXPGPATH]; @@ -6023,6 +6034,16 @@ parseServiceFile(const char *serviceFile, goto exit; } + if (strcmp(key, "servicefile") == 0) + { + libpq_append_error(errorMessage, + "nested servicefile specifications not supported in service file \"%s\", line %d", + serviceFile, + linenr); + result = 3; + goto exit; + } + /* * Set the parameter --- but don't override any previous * explicit setting. diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build index 292fecf3320..4714c253792 100644 --- a/src/interfaces/libpq/meson.build +++ b/src/interfaces/libpq/meson.build @@ -123,6 +123,7 @@ tests += { 't/004_load_balance_dns.pl', 't/005_negotiate_encryption.pl', 't/006_service.pl', + 't/007_servicefile_opt.pl', ], 'env': { 'with_ssl': ssl_library, diff --git a/src/interfaces/libpq/t/007_servicefile_opt.pl b/src/interfaces/libpq/t/007_servicefile_opt.pl new file mode 100644 index 00000000000..7d33bd18563 --- /dev/null +++ b/src/interfaces/libpq/t/007_servicefile_opt.pl @@ -0,0 +1,100 @@ +# Copyright (c) 2023-2024, PostgreSQL Global Development Group +use strict; +use warnings FATAL => 'all'; +use Config; +use PostgreSQL::Test::Utils; +use PostgreSQL::Test::Cluster; +use Test::More; + +# This tests "servicefile option" on connection string + +# Cluster setup which is shared for testing both load balancing methods +my $node = PostgreSQL::Test::Cluster->new('node'); + +# Create a data directory with initdb +$node->init(); + +# Start the PostgreSQL server +$node->start(); + +my $td = PostgreSQL::Test::Utils::tempdir; +my $srvfile = "$td/pgsrv.conf"; + +# Create a service file +open my $fh, '>', $srvfile or die $!; +if ($windows_os) { + + # Windows: use CRLF + print $fh "[my_srv]", "\r\n"; + print $fh join( "\r\n", split( ' ', $node->connstr ) ), "\r\n"; + + # Escape backslashes for use in connection string later + $srvfile =~ s/\\/\\\\/g; +} +else { + # Non-Windows: use LF + print $fh "[my_srv]", "\n"; + print $fh join( "\n", split( ' ', $node->connstr ) ), "\n"; +} +close $fh; + +# Check that servicefile option works as expected +{ + $node->connect_ok( + q{service=my_srv servicefile='} . $srvfile . q{'}, + 'service=my_srv servicefile=...', + sql => "SELECT 'connect1'", + expected_stdout => qr/connect1/ + ); + + # Escape slashes in servicefile path for use in connection string + # Consider that the servicefile path may contain backslashes on Windows + my $encoded_srvfile = $srvfile =~ s{([\\/])}{ + $1 eq '/' ? '%2F' : '%5C' + }ger; + + # Escape a colon in servicefile path of Windows + $encoded_srvfile =~ s/:/%3A/g; + + $node->connect_ok( + 'postgresql:///?service=my_srv&servicefile=' . $encoded_srvfile, + 'postgresql:///?service=my_srv&servicefile=...', + sql => "SELECT 'connect2'", + expected_stdout => qr/connect2/ + ); + + local $ENV{PGSERVICE} = 'my_srv'; + $node->connect_ok( + q{servicefile='} . $srvfile . q{'}, + 'envvar: PGSERVICE=my_srv + servicefile=...', + sql => "SELECT 'connect3'", + expected_stdout => qr/connect3/ + ); + + $node->connect_ok( + 'postgresql://?servicefile=' . $encoded_srvfile, + 'envvar: PGSERVICE=my_srv + postgresql://?servicefile=...', + sql => "SELECT 'connect4'", + expected_stdout => qr/connect4/ + ); +} + +# Check that servicefile option takes precedence over PGSERVICEFILE environment variable +{ + local $ENV{PGSERVICEFILE} = 'non-existent-file.conf'; + + $node->connect_fails( + 'service=my_srv', + 'service=... fails with wrong PGSERVICEFILE', + expected_stderr => qr/service file "non-existent-file\.conf" not found/ + ); + + $node->connect_ok( + q{service=my_srv servicefile='} . $srvfile . q{'}, + 'servicefile= takes precedence over PGSERVICEFILE', + sql => "SELECT 'connect5'", + expected_stdout => qr/connect5/ + ); +} + +done_testing();