Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/Npgsql/Internal/NpgsqlConnector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,7 @@ static async Task OpenCore(
{
await conn.RawOpen(sslMode, timeout, async, cancellationToken, isFirstAttempt).ConfigureAwait(false);

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

timeout.CheckAndApply(conn);
conn.WriteStartupMessage(username);
Expand Down Expand Up @@ -704,7 +704,7 @@ void WriteStartupMessage(string username)
WriteStartup(startupParams);
}

ValueTask<string> GetUsernameAsync(bool async, CancellationToken cancellationToken)
ValueTask<string> GetUsername(bool async, CancellationToken cancellationToken)
{
var username = Settings.Username;
if (username?.Length > 0)
Expand All @@ -720,9 +720,9 @@ ValueTask<string> GetUsernameAsync(bool async, CancellationToken cancellationTok
return new(username);
}

return GetUsernameAsyncInternal();
return GetUsernameInternal();

async ValueTask<string> GetUsernameAsyncInternal()
async ValueTask<string> GetUsernameInternal()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
Expand Down
79 changes: 10 additions & 69 deletions src/Npgsql/KerberosUsernameProvider.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System;
using System.Diagnostics;
using System.IO;
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
Expand All @@ -21,71 +19,26 @@ sealed class KerberosUsernameProvider
{
if (_performedDetection)
return new(includeRealm ? _principalWithRealm : _principalWithoutRealm);
var klistPath = FindInPath("klist");
if (klistPath == null)
{
connectionLogger.LogDebug("klist not found in PATH, skipping Kerberos username detection");
return new((string?)null);
}
var processStartInfo = new ProcessStartInfo
{
FileName = klistPath,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe leave the part above here as sync function and move the part below to a local async function (as it was before).
That way access to the cached values works without async state machine.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

var process = Process.Start(processStartInfo);
if (process is null)
{
connectionLogger.LogDebug("klist process could not be started");
return new((string?)null);
}

return GetUsernameAsyncInternal();
return GetUsernameInternal();

#pragma warning disable CS1998
async ValueTask<string?> GetUsernameAsyncInternal()
#pragma warning restore CS1998
async ValueTask<string?> GetUsernameInternal()
{
#if NET5_0_OR_GREATER
if (async)
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
else
// ReSharper disable once MethodHasAsyncOverloadWithCancellation
process.WaitForExit();
#else
// ReSharper disable once MethodHasAsyncOverload
process.WaitForExit();
#endif

if (process.ExitCode != 0)
var lines = await PostgresEnvironment.ExecuteCommand("klist", async, logger: connectionLogger, cancellationToken: cancellationToken).ConfigureAwait(false);
if (lines is null)
{
connectionLogger.LogDebug($"klist exited with code {process.ExitCode}: {process.StandardError.ReadToEnd()}");
connectionLogger.LogDebug("Skipping Kerberos username detection");
return null;
}

var line = default(string);
for (var i = 0; i < 2; i++)
// ReSharper disable once MethodHasAsyncOverload
#if NET7_0_OR_GREATER
if ((line = async ? await process.StandardOutput.ReadLineAsync(cancellationToken).ConfigureAwait(false) : process.StandardOutput.ReadLine()) == null)
#elif NET5_0_OR_GREATER
if ((line = async ? await process.StandardOutput.ReadLineAsync().ConfigureAwait(false) : process.StandardOutput.ReadLine()) == null)
#else
if ((line = process.StandardOutput.ReadLine()) == null)
#endif
{
connectionLogger.LogDebug("Unexpected output from klist, aborting Kerberos username detection");
return null;
}

return ParseKListOutput(line!, includeRealm, connectionLogger);
return ParseKListOutput(lines, includeRealm, connectionLogger);
}
}

static string? ParseKListOutput(string line, bool includeRealm, ILogger connectionLogger)
static string? ParseKListOutput(string[] lines, bool includeRealm, ILogger connectionLogger)
{
if (lines.Length < 2) return null;
var line = lines[1];
var colonIndex = line.IndexOf(':');
var colonLastIndex = line.LastIndexOf(':');
if (colonIndex == -1 || colonIndex != colonLastIndex)
Expand All @@ -110,16 +63,4 @@ sealed class KerberosUsernameProvider
_performedDetection = true;
return includeRealm ? _principalWithRealm : _principalWithoutRealm;
}

static string? FindInPath(string name)
{
foreach (var p in Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? Array.Empty<string>())
{
var path = Path.Combine(p, name);
if (File.Exists(path))
return path;
}

return null;
}
}
136 changes: 136 additions & 0 deletions src/Npgsql/NpgsqlConnectionStringBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1323,6 +1323,24 @@ public ArrayNullabilityMode ArrayNullabilityMode

ArrayNullabilityMode _arrayNullabilityMode;

/// <summary>
/// Set service name to retrieve associated PostgreSQL configuration parameters from the pg_service.conf file.
/// </summary>
[Category("Advanced")]
[Description("Set service name to retrieve associated PostgreSQL configuration parameters from the pg_service.conf file.")]
[DisplayName("Service")]
[NpgsqlConnectionStringProperty]
public string? Service
{
get => _service;
set
{
_service = value;
SetValue(nameof(Service), value);
}
}
string? _service;

#endregion

#region Multiplexing
Expand Down Expand Up @@ -1578,6 +1596,8 @@ public bool TrustServerCertificate

internal void PostProcessAndValidate()
{
LoadConfigurationFromPgServiceFile();

if (string.IsNullOrWhiteSpace(Host))
throw new ArgumentException("Host can't be null");
if (Multiplexing && !Pooling)
Expand Down Expand Up @@ -1762,6 +1782,122 @@ protected override void GetProperties(Hashtable propertyDescriptors)
}

#endregion

#region Load from Postgres service file

void LoadConfigurationFromPgServiceFile()
{
Service ??= PostgresEnvironment.Service;
if (Service is null)
{
return;
}

if (!LoadConfigurationFromIniFile(PostgresEnvironment.UserServiceFile))
LoadConfigurationFromIniFile(PostgresEnvironment.SystemServiceFile);

bool LoadConfigurationFromIniFile(string? filePath)
{
if (filePath is null || !File.Exists(filePath))
{
return false;
}

var settings = ReadIniFile(filePath);
if (settings is null)
{
return false;
}

foreach (var kv in settings)
{
if (ContainsKey(kv.Key) && !Keys.Contains(kv.Key))
{
this[kv.Key] = kv.Value;
}
}

return true;
}

Dictionary<string, string>? ReadIniFile(string filePath)
{
Dictionary<string, string>? settings = default;

var bytes = File.ReadAllBytes(filePath);
var mem = new MemoryStream(bytes);
using var reader = new StreamReader(mem);
while (reader.ReadLine() is { } rawLine)
{
var line = rawLine.Trim();

// Ignore blank lines
if (string.IsNullOrWhiteSpace(line))
{
continue;
}

// Ignore comments
if (line[0] is ';' or '#' or '/')
{
continue;
}

// [Section:header]
if (line[0] == '[' && line[line.Length - 1] == ']')
Comment thread
vonzshik marked this conversation as resolved.
{
// All settings for the specific service has been retrieved
if (settings is not null)
{
break;
}

// remove the brackets
var sectionPrefix = line.Substring(1, line.Length - 2).Trim();
// Check whether it is the specified service
if (sectionPrefix == Service)
{
settings = [];
}

continue;
}

// Skip lines if not in the section for specified service
if (settings is null)
{
continue;
}

// key = value OR "value"
var separator = line.IndexOf('=');
if (separator <= 0)
{
throw new FormatException($"Unrecognized line format: '{rawLine}'.");
}

var key = line.Substring(0, separator).Trim();
var value = line.Substring(separator + 1).Trim();

// Remove quotes
if (value.Length > 1 && value[0] == '"' && value[value.Length - 1] == '"')
{
value = value.Substring(1, value.Length - 2);
}

if (settings.ContainsKey(key))
{
throw new FormatException($"A duplicate key '{key}' was found.");
}

settings[key] = value;
}

return settings;
}
}

#endregion
}

#region Attributes
Expand Down
Loading