Skip to content

Commit adafb88

Browse files
committed
Support for pg_service.conf
1 parent 95b7a45 commit adafb88

File tree

6 files changed

+418
-75
lines changed

6 files changed

+418
-75
lines changed

src/Npgsql/Internal/NpgsqlConnector.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,7 @@ static async Task OpenCore(
581581
{
582582
await conn.RawOpen(sslMode, timeout, async, cancellationToken, isFirstAttempt).ConfigureAwait(false);
583583

584-
var username = await conn.GetUsernameAsync(async, cancellationToken).ConfigureAwait(false);
584+
var username = await conn.GetUsername(async, cancellationToken).ConfigureAwait(false);
585585

586586
timeout.CheckAndApply(conn);
587587
conn.WriteStartupMessage(username);
@@ -713,7 +713,7 @@ void WriteStartupMessage(string username)
713713
WriteStartup(startupParams);
714714
}
715715

716-
ValueTask<string> GetUsernameAsync(bool async, CancellationToken cancellationToken)
716+
ValueTask<string> GetUsername(bool async, CancellationToken cancellationToken)
717717
{
718718
var username = Settings.Username;
719719
if (username?.Length > 0)
@@ -729,9 +729,9 @@ ValueTask<string> GetUsernameAsync(bool async, CancellationToken cancellationTok
729729
return new(username);
730730
}
731731

732-
return GetUsernameAsyncInternal();
732+
return GetUsernameInternal();
733733

734-
async ValueTask<string> GetUsernameAsyncInternal()
734+
async ValueTask<string> GetUsernameInternal()
735735
{
736736
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
737737
{
Lines changed: 10 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
using System;
2-
using System.Diagnostics;
3-
using System.IO;
1+
using System;
42
using System.Threading;
53
using System.Threading.Tasks;
64
using Microsoft.Extensions.Logging;
@@ -21,71 +19,26 @@ sealed class KerberosUsernameProvider
2119
{
2220
if (_performedDetection)
2321
return new(includeRealm ? _principalWithRealm : _principalWithoutRealm);
24-
var klistPath = FindInPath("klist");
25-
if (klistPath == null)
26-
{
27-
connectionLogger.LogDebug("klist not found in PATH, skipping Kerberos username detection");
28-
return new((string?)null);
29-
}
30-
var processStartInfo = new ProcessStartInfo
31-
{
32-
FileName = klistPath,
33-
RedirectStandardOutput = true,
34-
RedirectStandardError = true,
35-
UseShellExecute = false
36-
};
3722

38-
var process = Process.Start(processStartInfo);
39-
if (process is null)
40-
{
41-
connectionLogger.LogDebug("klist process could not be started");
42-
return new((string?)null);
43-
}
44-
45-
return GetUsernameAsyncInternal();
23+
return GetUsernameInternal();
4624

47-
#pragma warning disable CS1998
48-
async ValueTask<string?> GetUsernameAsyncInternal()
49-
#pragma warning restore CS1998
25+
async ValueTask<string?> GetUsernameInternal()
5026
{
51-
#if NET5_0_OR_GREATER
52-
if (async)
53-
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
54-
else
55-
// ReSharper disable once MethodHasAsyncOverloadWithCancellation
56-
process.WaitForExit();
57-
#else
58-
// ReSharper disable once MethodHasAsyncOverload
59-
process.WaitForExit();
60-
#endif
61-
62-
if (process.ExitCode != 0)
27+
var lines = await PostgresEnvironment.ExecuteCommand("klist", async, logger: connectionLogger, cancellationToken: cancellationToken).ConfigureAwait(false);
28+
if (lines is null)
6329
{
64-
connectionLogger.LogDebug($"klist exited with code {process.ExitCode}: {process.StandardError.ReadToEnd()}");
30+
connectionLogger.LogDebug("Skipping Kerberos username detection");
6531
return null;
6632
}
6733

68-
var line = default(string);
69-
for (var i = 0; i < 2; i++)
70-
// ReSharper disable once MethodHasAsyncOverload
71-
#if NET7_0_OR_GREATER
72-
if ((line = async ? await process.StandardOutput.ReadLineAsync(cancellationToken).ConfigureAwait(false) : process.StandardOutput.ReadLine()) == null)
73-
#elif NET5_0_OR_GREATER
74-
if ((line = async ? await process.StandardOutput.ReadLineAsync().ConfigureAwait(false) : process.StandardOutput.ReadLine()) == null)
75-
#else
76-
if ((line = process.StandardOutput.ReadLine()) == null)
77-
#endif
78-
{
79-
connectionLogger.LogDebug("Unexpected output from klist, aborting Kerberos username detection");
80-
return null;
81-
}
82-
83-
return ParseKListOutput(line!, includeRealm, connectionLogger);
34+
return ParseKListOutput(lines, includeRealm, connectionLogger);
8435
}
8536
}
8637

87-
static string? ParseKListOutput(string line, bool includeRealm, ILogger connectionLogger)
38+
static string? ParseKListOutput(string[] lines, bool includeRealm, ILogger connectionLogger)
8839
{
40+
if (lines.Length < 2) return null;
41+
var line = lines[1];
8942
var colonIndex = line.IndexOf(':');
9043
var colonLastIndex = line.LastIndexOf(':');
9144
if (colonIndex == -1 || colonIndex != colonLastIndex)
@@ -110,16 +63,4 @@ sealed class KerberosUsernameProvider
11063
_performedDetection = true;
11164
return includeRealm ? _principalWithRealm : _principalWithoutRealm;
11265
}
113-
114-
static string? FindInPath(string name)
115-
{
116-
foreach (var p in Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? Array.Empty<string>())
117-
{
118-
var path = Path.Combine(p, name);
119-
if (File.Exists(path))
120-
return path;
121-
}
122-
123-
return null;
124-
}
12566
}

src/Npgsql/NpgsqlConnectionStringBuilder.cs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1323,6 +1323,24 @@ public ArrayNullabilityMode ArrayNullabilityMode
13231323

13241324
ArrayNullabilityMode _arrayNullabilityMode;
13251325

1326+
/// <summary>
1327+
/// Set service name to retrieve associated PostgreSQL configuration parameters from the pg_service.conf file.
1328+
/// </summary>
1329+
[Category("Advanced")]
1330+
[Description("Set service name to retrieve associated PostgreSQL configuration parameters from the pg_service.conf file.")]
1331+
[DisplayName("Service")]
1332+
[NpgsqlConnectionStringProperty]
1333+
public string? Service
1334+
{
1335+
get => _service;
1336+
set
1337+
{
1338+
_service = value;
1339+
SetValue(nameof(Service), value);
1340+
}
1341+
}
1342+
string? _service;
1343+
13261344
#endregion
13271345

13281346
#region Multiplexing
@@ -1578,6 +1596,8 @@ public bool TrustServerCertificate
15781596

15791597
internal void PostProcessAndValidate()
15801598
{
1599+
LoadConfigurationFromPgServiceFile();
1600+
15811601
if (string.IsNullOrWhiteSpace(Host))
15821602
throw new ArgumentException("Host can't be null");
15831603
if (Multiplexing && !Pooling)
@@ -1762,6 +1782,120 @@ protected override void GetProperties(Hashtable propertyDescriptors)
17621782
}
17631783

17641784
#endregion
1785+
1786+
#region Load from Postgres service file
1787+
1788+
void LoadConfigurationFromPgServiceFile()
1789+
{
1790+
Service ??= PostgresEnvironment.Service;
1791+
if (Service is null)
1792+
{
1793+
return;
1794+
}
1795+
1796+
if (!LoadConfigurationFromIniFile(PostgresEnvironment.UserServiceFile))
1797+
LoadConfigurationFromIniFile(PostgresEnvironment.SystemServiceFile);
1798+
1799+
bool LoadConfigurationFromIniFile(string? filePath)
1800+
{
1801+
if (filePath is null || !File.Exists(filePath))
1802+
{
1803+
return false;
1804+
}
1805+
1806+
var settings = ReadIniFile(filePath);
1807+
if (settings is null)
1808+
{
1809+
return false;
1810+
}
1811+
1812+
foreach (var kv in settings)
1813+
{
1814+
if (ContainsKey(kv.Key) && !Keys.Contains(kv.Key))
1815+
{
1816+
this[kv.Key] = kv.Value;
1817+
}
1818+
}
1819+
1820+
return true;
1821+
}
1822+
1823+
Dictionary<string, string>? ReadIniFile(string filePath)
1824+
{
1825+
var lines = File.ReadAllLines(filePath);
1826+
1827+
Dictionary<string, string>? settings = default;
1828+
foreach (var rawLine in lines)
1829+
{
1830+
var line = rawLine.Trim();
1831+
1832+
// Ignore blank lines
1833+
if (string.IsNullOrWhiteSpace(line))
1834+
{
1835+
continue;
1836+
}
1837+
1838+
// Ignore comments
1839+
if (line[0] is ';' or '#' or '/')
1840+
{
1841+
continue;
1842+
}
1843+
1844+
// [Section:header]
1845+
if (line[0] == '[' && line[line.Length - 1] == ']')
1846+
{
1847+
// All settings for the specific service has been retrieved
1848+
if (settings is not null)
1849+
{
1850+
break;
1851+
}
1852+
1853+
// remove the brackets
1854+
var sectionPrefix = line.Substring(1, line.Length - 2).Trim();
1855+
// Check whether it is the specified service
1856+
if (sectionPrefix == Service)
1857+
{
1858+
settings = new Dictionary<string, string>();
1859+
}
1860+
1861+
continue;
1862+
}
1863+
1864+
// Skip lines if not in the section for specified service
1865+
if (settings is null)
1866+
{
1867+
continue;
1868+
}
1869+
1870+
// key = value OR "value"
1871+
var separator = line.IndexOf('=');
1872+
if (separator <= 0)
1873+
{
1874+
throw new FormatException($"Unrecognized line format: '{rawLine}'.");
1875+
}
1876+
1877+
var key = line.Substring(0, separator).Trim();
1878+
var value = line.Substring(separator + 1).Trim();
1879+
1880+
// Remove quotes
1881+
if (value.Length > 1 && value[0] == '"' && value[value.Length - 1] == '"')
1882+
{
1883+
value = value.Substring(1, value.Length - 2);
1884+
}
1885+
1886+
if (settings.ContainsKey(key))
1887+
{
1888+
throw new FormatException($"A duplicate key '{key}' was found.");
1889+
}
1890+
1891+
settings[key] = value;
1892+
}
1893+
1894+
return settings;
1895+
}
1896+
}
1897+
1898+
#endregion
17651899
}
17661900

17671901
#region Attributes

0 commit comments

Comments
 (0)