From 3f60cf47e50884aec2e77ac9a65b44cd84f63298 Mon Sep 17 00:00:00 2001 From: Ryo Kanbayashi Date: Mon, 9 Jun 2025 22:20:04 +0900 Subject: [PATCH v9] servicefile option usage on connection string feature and its tests. --- doc/src/sgml/libpq.sgml | 37 ++++++++++- doc/src/sgml/ref/psql-ref.sgml | 9 +++ src/bin/psql/command.c | 2 + src/interfaces/libpq/exports.txt | 11 +-- src/interfaces/libpq/fe-connect.c | 39 +++++++++-- src/interfaces/libpq/libpq-fe.h | 1 + src/interfaces/libpq/libpq-int.h | 1 + src/interfaces/libpq/t/006_service.pl | 96 +++++++++++++++++++++++++++ 8 files changed, 186 insertions(+), 10 deletions(-) diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index 695fe958c3e..5e410ae8004 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -2320,6 +2320,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 @@ -2760,6 +2773,25 @@ char *PQservice(const PGconn *conn); + + PQserviceFile + + + + Returns the service file name of the active connection. + +char *PQserviceFile(const PGconn *conn); + + + + + returns NULL if the + conn argument is NULL. + Otherwise, if there was no service file provided, it returns an empty string. + + + + PQttyPQtty @@ -9166,6 +9198,8 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough) Defaults to ~/.pg_service.conf, or %APPDATA%\postgresql\.pg_service.conf on Microsoft Windows. + This environment variable behaves the same as the connection parameter. @@ -9596,7 +9630,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/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 8f7d8758ca0..e55f144f9a0 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -4610,6 +4610,15 @@ bar + + SERVICEFILE + + + The service file name, if applicable. + + + + SHELL_ERROR diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 81a5ba844ba..4305e398b34 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -4487,6 +4487,7 @@ SyncVariables(void) SetVariable(pset.vars, "DBNAME", PQdb(pset.db)); SetVariable(pset.vars, "SERVICE", PQservice(pset.db)); + SetVariable(pset.vars, "SERVICEFILE", PQserviceFile(pset.db)); SetVariable(pset.vars, "USER", PQuser(pset.db)); SetVariable(pset.vars, "HOST", PQhost(pset.db)); SetVariable(pset.vars, "PORT", PQport(pset.db)); @@ -4521,6 +4522,7 @@ UnsyncVariables(void) { SetVariable(pset.vars, "DBNAME", NULL); SetVariable(pset.vars, "SERVICE", NULL); + SetVariable(pset.vars, "SERVICEFILE", NULL); SetVariable(pset.vars, "USER", NULL); SetVariable(pset.vars, "HOST", NULL); SetVariable(pset.vars, "PORT", NULL); diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 0625cf39e9a..72b6584e795 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -206,8 +206,9 @@ PQsocketPoll 203 PQsetChunkedRowsMode 204 PQgetCurrentTimeUSec 205 PQservice 206 -PQsetAuthDataHook 207 -PQgetAuthDataHook 208 -PQdefaultAuthDataHook 209 -PQfullProtocolVersion 210 -appendPQExpBufferVA 211 +PQserviceFile 207 +PQsetAuthDataHook 208 +PQgetAuthDataHook 209 +PQdefaultAuthDataHook 210 +PQfullProtocolVersion 211 +appendPQExpBufferVA 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index ccb01aad361..2ac783e5e00 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -201,6 +201,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = { "Database-Service", "", 20, offsetof(struct pg_conn, pgservice)}, + {"servicefile", "PGSERVICEFILE", NULL, NULL, + "Database-Service-File", "", 64, + offsetof(struct pg_conn, pgservicefile)}, + {"user", "PGUSER", NULL, NULL, "Database-User", "", 20, offsetof(struct pg_conn, pguser)}, @@ -5062,6 +5066,7 @@ freePGconn(PGconn *conn) free(conn->dbName); free(conn->replication); free(conn->pgservice); + free(conn->pgservicefile); free(conn->pguser); if (conn->pgpass) { @@ -5914,6 +5919,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; @@ -5933,11 +5939,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]; @@ -6092,7 +6105,17 @@ parseServiceFile(const char *serviceFile, if (strcmp(key, "service") == 0) { libpq_append_error(errorMessage, - "nested service specifications not supported in service file \"%s\", line %d", + "nested \"service\" specifications not supported in service file \"%s\", line %d", + serviceFile, + linenr); + result = 3; + 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; @@ -7469,6 +7492,14 @@ PQservice(const PGconn *conn) return conn->pgservice; } +char * +PQserviceFile(const PGconn *conn) +{ + if (!conn) + return NULL; + return conn->pgservicefile; +} + char * PQuser(const PGconn *conn) { diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 7d3a9df6fd5..c7c443531c2 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -401,6 +401,7 @@ extern int PQrequestCancel(PGconn *conn); /* Accessor functions for PGconn objects */ extern char *PQdb(const PGconn *conn); extern char *PQservice(const PGconn *conn); +extern char *PQserviceFile(const PGconn *conn); extern char *PQuser(const PGconn *conn); extern char *PQpass(const PGconn *conn); extern char *PQhost(const PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index a6cfd7f5c9d..5ae4e88f0b7 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -389,6 +389,7 @@ struct pg_conn char *dbName; /* database name */ char *replication; /* connect as the replication standby? */ char *pgservice; /* Postgres service, if any */ + char *pgservicefile; /* path to a service file containing service(s) */ char *pguser; /* Postgres username and password, if any */ char *pgpass; char *pgpassfile; /* path to a file containing password(s) */ diff --git a/src/interfaces/libpq/t/006_service.pl b/src/interfaces/libpq/t/006_service.pl index 4fe5adc5c2a..b27b3c17d13 100644 --- a/src/interfaces/libpq/t/006_service.pl +++ b/src/interfaces/libpq/t/006_service.pl @@ -47,6 +47,21 @@ my $srvfile_default = "$td/pg_service.conf"; # Missing service file. my $srvfile_missing = "$td/pg_service_missing.conf"; +# "service" param included service file (invalid) +# including contents of pg_service_valid.conf and a nested service option +my $srvfile_service_nested = "$td/pg_service_service_nested.conf"; +copy($srvfile_valid, $srvfile_service_nested) or + die "Could not copy $srvfile_valid to $srvfile_service_nested: $!"; +append_to_file($srvfile_service_nested, 'service=tmp_srv' . $newline); + +# "servicefile" param included service file (invalid) +# including contents of pg_service_valid.conf and a nested servicefile option +my $srvfile_servicefile_nested = "$td/pg_service_servicefile_nested.conf"; +copy($srvfile_valid, $srvfile_servicefile_nested) or + die "Could not copy $srvfile_valid to $srvfile_servicefile_nested: $!"; +append_to_file($srvfile_servicefile_nested, 'servicefile=' . $srvfile_default . $newline); + + # Set the fallback directory lookup of the service file to the temporary # directory of this test. PGSYSCONFDIR is used if the service file # defined in PGSERVICEFILE cannot be found, or when a service file is @@ -146,6 +161,87 @@ local $ENV{PGSERVICEFILE} = "$srvfile_empty"; unlink($srvfile_default); } +# Use correct escaped path for Windows. +my $srvfile_win_cared = $srvfile_valid; +$srvfile_win_cared =~ s/\\/\\\\/g; + +# Check that servicefile option works as expected +{ + $dummy_node->connect_ok( + q{service=my_srv servicefile='} . $srvfile_win_cared . q{'}, + 'service=my_srv servicefile=...', + sql => "SELECT 'connect3'", + expected_stdout => qr/connect3/ + ); + + # Encode slashes and backslash + my $encoded_srvfile = $srvfile_valid =~ s{([\\/])}{ + $1 eq '/' ? '%2F' : '%5C' + }ger; + + # Additionally encode a colon in servicefile path of Windows + $encoded_srvfile =~ s/:/%3A/g; + + $dummy_node->connect_ok( + 'postgresql:///?service=my_srv&servicefile=' . $encoded_srvfile, + 'postgresql:///?service=my_srv&servicefile=...', + sql => "SELECT 'connect4'", + expected_stdout => qr/connect4/ + ); + + local $ENV{PGSERVICE} = 'my_srv'; + $dummy_node->connect_ok( + q{servicefile='} . $srvfile_win_cared . q{'}, + 'envvar: PGSERVICE=my_srv + servicefile=...', + sql => "SELECT 'connect5'", + expected_stdout => qr/connect5/ + ); + + $dummy_node->connect_ok( + 'postgresql://?servicefile=' . $encoded_srvfile, + 'envvar: PGSERVICE=my_srv + postgresql://?servicefile=...', + sql => "SELECT 'connect6'", + expected_stdout => qr/connect6/ + ); +} + +# Check that servicefile option takes precedence over PGSERVICEFILE environment variable +{ + local $ENV{PGSERVICEFILE} = 'non-existent-file.conf'; + + $dummy_node->connect_fails( + 'service=my_srv', + 'service=... fails with wrong PGSERVICEFILE', + expected_stderr => qr/service file "non-existent-file\.conf" not found/ + ); + + $dummy_node->connect_ok( + q{service=my_srv servicefile='} . $srvfile_win_cared . q{'}, + 'servicefile= takes precedence over PGSERVICEFILE', + sql => "SELECT 'connect7'", + expected_stdout => qr/connect7/ + ); +} + +# Check that service file which contains nested service and servicefile options fails +{ + local $ENV{PGSERVICEFILE} = $srvfile_service_nested; + + $dummy_node->connect_fails( + 'service=my_srv', + 'service=... fails with nested service option in service file', + expected_stderr => qr/nested "service" specifications not supported in service file/ + ); + + local $ENV{PGSERVICEFILE} = $srvfile_servicefile_nested; + + $dummy_node->connect_fails( + 'service=my_srv', + 'servicefile=... fails with nested service option in service file', + expected_stderr => qr/nested "servicefile" specifications not supported in service file/ + ); +} + $node->teardown_node; done_testing(); -- 2.45.1.windows.1