-
Notifications
You must be signed in to change notification settings - Fork 874
Expand file tree
/
Copy pathPgPassFile.cs
More file actions
201 lines (172 loc) · 7.58 KB
/
PgPassFile.cs
File metadata and controls
201 lines (172 loc) · 7.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace Npgsql;
/// <summary>
/// Represents a .pgpass file, which contains passwords for noninteractive connections
/// </summary>
sealed class PgPassFile
{
#region Properties
/// <summary>
/// File name being parsed for credentials
/// </summary>
internal string FileName { get; }
#endregion
#region Construction
/// <summary>
/// Initializes a new instance of the <see cref="PgPassFile"/> class
/// </summary>
/// <param name="fileName"></param>
public PgPassFile(string fileName)
=> FileName = fileName;
#endregion
/// <summary>
/// Parses file content and gets all credentials from the file
/// </summary>
/// <returns><see cref="IEnumerable{PgPassEntry}"/> corresponding to all lines in the .pgpass file</returns>
internal IEnumerable<Entry> Entries
{
get
{
var bytes = File.ReadAllBytes(FileName);
var mem = new MemoryStream(bytes);
using var reader = new StreamReader(mem);
while (reader.ReadLine() is { } l)
{
var line = l.Trim();
if (line.Length > 0 && line[0] != '#')
yield return Entry.Parse(line);
}
}
}
/// <summary>
/// Searches queries loaded from .PGPASS file to find first entry matching the provided parameters.
/// </summary>
/// <param name="host">Hostname to query. Use null to match any.</param>
/// <param name="port">Port to query. Use null to match any.</param>
/// <param name="database">Database to query. Use null to match any.</param>
/// <param name="username">User name to query. Use null to match any.</param>
/// <returns>Matching <see cref="Entry"/> if match was found. Otherwise, returns null.</returns>
internal Entry? GetFirstMatchingEntry(string? host = null, int? port = null, string? database = null, string? username = null)
{
foreach (var entry in Entries)
if (entry.IsMatch(host, port, database, username))
return entry;
return null;
}
/// <summary>
/// Represents a hostname, port, database, username, and password combination that has been retrieved from a .pgpass file
/// </summary>
internal sealed class Entry
{
#region Fields and Properties
/// <summary>
/// Hostname parsed from the .pgpass file
/// </summary>
internal string? Host { get; }
/// <summary>
/// Port parsed from the .pgpass file
/// </summary>
internal int? Port { get; }
/// <summary>
/// Database parsed from the .pgpass file
/// </summary>
internal string? Database { get; }
/// <summary>
/// User name parsed from the .pgpass file
/// </summary>
internal string? Username { get; }
/// <summary>
/// Password parsed from the .pgpass file
/// </summary>
internal string? Password { get; }
#endregion
#region Construction / Initialization
/// <summary>
/// This class represents an entry from the .pgpass file
/// </summary>
/// <param name="host">Hostname parsed from the .pgpass file</param>
/// <param name="port">Port parsed from the .pgpass file</param>
/// <param name="database">Database parsed from the .pgpass file</param>
/// <param name="username">User name parsed from the .pgpass file</param>
/// <param name="password">Password parsed from the .pgpass file</param>
Entry(string? host, int? port, string? database, string? username, string? password)
{
Host = host;
Port = port;
Database = database;
Username = username;
Password = password;
}
/// <summary>
/// Creates new <see cref="Entry"/> based on string in the format hostname:port:database:username:password. The : and \ characters should be escaped with a \.
/// </summary>
/// <param name="serializedEntry">string for the entry from the pgpass file</param>
/// <returns>New instance of <see cref="Entry"/> for the string</returns>
/// <exception cref="FormatException">Entry is not formatted as hostname:port:database:username:password or non-wildcard port is not a number</exception>
internal static Entry Parse(string serializedEntry)
{
var parts = new List<string?>(5);
var builder = new StringBuilder();
for (var pos = 0; pos < serializedEntry.Length; pos++)
{
var c = serializedEntry[pos];
switch (c)
{
case '\\' when pos < serializedEntry.Length - 1:
// Strip backslash before colon or backslash, otherwise preserve it
c = serializedEntry[++pos];
if (c is not (':' or '\\'))
{
builder.Append('\\');
}
builder.Append(c);
continue;
case ':':
var part = builder.ToString();
parts.Add(part == "*" ? null : part);
builder.Clear();
continue;
default:
builder.Append(c);
continue;
}
}
var lastPart = builder.ToString();
parts.Add(lastPart == "*" ? null : lastPart);
if (parts.Count != 5)
throw new FormatException("pgpass entry was not well-formed. Please ensure all non-comment entries are formatted as hostname:port:database:username:password. If colon is included, it must be escaped like \\:.");
int? port = null;
if (parts[1] != null)
{
if (!int.TryParse(parts[1], out var tempPort))
throw new FormatException("pgpass entry was not formatted correctly. Port must be a valid integer.");
port = tempPort;
}
return new Entry(parts[0], port, parts[2], parts[3], parts[4]);
}
#endregion
/// <summary>
/// Checks whether this <see cref="Entry"/> matches the parameters supplied
/// </summary>
/// <param name="host">Hostname to check against this entry</param>
/// <param name="port">Port to check against this entry</param>
/// <param name="database">Database to check against this entry</param>
/// <param name="username">Username to check against this entry</param>
/// <returns>True if the entry is a match. False otherwise.</returns>
internal bool IsMatch(string? host, int? port, string? database, string? username) =>
AreValuesMatched(host, Host) && AreValuesMatched(port, Port) && AreValuesMatched(database, Database) && AreValuesMatched(username, Username);
/// <summary>
/// Checks if 2 strings are a match for a <see cref="Entry"/> considering that either value can be a wildcard (*)
/// </summary>
/// <param name="query">Value being searched</param>
/// <param name="actual">Value from the PGPASS entry</param>
/// <returns>True if the values are a match. False otherwise.</returns>
bool AreValuesMatched(string? query, string? actual)
=> query == actual || actual == null || query == null;
bool AreValuesMatched(int? query, int? actual)
=> query == actual || actual == null || query == null;
}
}