9
9
import collections
10
10
import getpass
11
11
import os
12
+ import pathlib
12
13
import platform
14
+ import re
13
15
import socket
16
+ import stat
14
17
import struct
15
18
import time
19
+ import typing
16
20
import urllib .parse
21
+ import warnings
17
22
23
+ from . import compat
18
24
from . import exceptions
19
25
from . import protocol
20
26
44
50
_system = platform .uname ().system
45
51
46
52
53
+ if _system == 'Windows' :
54
+ PGPASSFILE = 'pgpass.conf'
55
+ else :
56
+ PGPASSFILE = '.pgpass'
57
+
58
+
59
+ def _read_password_file (passfile : pathlib .Path ) \
60
+ -> typing .List [typing .Tuple [str , ...]]:
61
+
62
+ if not passfile .is_file ():
63
+ warnings .warn (
64
+ 'password file {!r} is not a plain file' .format (passfile ))
65
+
66
+ return None
67
+
68
+ if _system != 'Windows' :
69
+ if passfile .stat ().st_mode & (stat .S_IRWXG | stat .S_IRWXO ):
70
+ warnings .warn (
71
+ 'password file {!r} has group or world access; '
72
+ 'permissions should be u=rw (0600) or less' .format (passfile ))
73
+
74
+ return None
75
+
76
+ passtab = []
77
+
78
+ try :
79
+ with passfile .open ('rt' ) as f :
80
+ for line in f :
81
+ line = line .strip ()
82
+ if not line or line .startswith ('#' ):
83
+ # Skip empty lines and comments.
84
+ continue
85
+ # Backslash escapes both itself and the colon,
86
+ # which is a record separator.
87
+ line = line .replace (R'\\' , '\n ' )
88
+ passtab .append (tuple (
89
+ p .replace ('\n ' , R'\\' )
90
+ for p in re .split (r'(?<!\\):' , line , maxsplit = 4 )
91
+ ))
92
+ except IOError :
93
+ pass
94
+
95
+ return passtab
96
+
97
+
98
+ def _read_password_from_pgpass (
99
+ * , passfile : typing .Optional [pathlib .Path ],
100
+ hosts : typing .List [typing .Union [str , typing .Tuple [str , int ]]],
101
+ port : int , database : str , user : str ):
102
+ """Parse the pgpass file and return the matching password.
103
+
104
+ :return:
105
+ Password string, if found, ``None`` otherwise.
106
+ """
107
+
108
+ if not passfile .exists ():
109
+ return None
110
+
111
+ passtab = _read_password_file (passfile )
112
+ if not passtab :
113
+ return None
114
+
115
+ for host in hosts :
116
+ if host .startswith ('/' ):
117
+ # Unix sockets get normalized into 'localhost'
118
+ host = 'localhost'
119
+
120
+ for phost , pport , pdatabase , puser , ppassword in passtab :
121
+ if phost != '*' and phost != host :
122
+ continue
123
+ if pport != '*' and pport != str (port ):
124
+ continue
125
+ if pdatabase != '*' and pdatabase != database :
126
+ continue
127
+ if puser != '*' and puser != user :
128
+ continue
129
+
130
+ # Found a match.
131
+ return ppassword
132
+
133
+ return None
134
+
135
+
47
136
def _parse_connect_dsn_and_args (* , dsn , host , port , user ,
48
- password , database , ssl , connect_timeout ,
49
- server_settings ):
137
+ password , passfile , database , ssl ,
138
+ connect_timeout , server_settings ):
50
139
if host is not None and not isinstance (host , str ):
51
140
raise TypeError (
52
141
'host argument is expected to be str, got {!r}' .format (
@@ -113,6 +202,11 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user,
113
202
if password is None :
114
203
password = val
115
204
205
+ if 'passfile' in query :
206
+ val = query .pop ('passfile' )
207
+ if passfile is None :
208
+ passfile = val
209
+
116
210
if query :
117
211
if server_settings is None :
118
212
server_settings = query
@@ -123,10 +217,14 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user,
123
217
# https://www.postgresql.org/docs/current/static/libpq-envars.html
124
218
# Note that env values may be an empty string in cases when
125
219
# the variable is "unset" by setting it to an empty value
126
- #
220
+ # `auth_hosts` is the version of host information for the purposes
221
+ # of reading the pgpass file.
222
+ auth_hosts = None
127
223
if host is None :
128
224
host = os .getenv ('PGHOST' )
129
225
if not host :
226
+ auth_hosts = ['localhost' ]
227
+
130
228
if _system == 'Windows' :
131
229
host = ['localhost' ]
132
230
else :
@@ -137,6 +235,9 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user,
137
235
if not isinstance (host , list ):
138
236
host = [host ]
139
237
238
+ if auth_hosts is None :
239
+ auth_hosts = host
240
+
140
241
if port is None :
141
242
port = os .getenv ('PGPORT' )
142
243
if port :
@@ -168,6 +269,24 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user,
168
269
raise exceptions .InterfaceError (
169
270
'could not determine database name to connect to' )
170
271
272
+ if password is None :
273
+ if passfile is None :
274
+ passfile = os .getenv ('PGPASSFILE' )
275
+
276
+ if passfile is None :
277
+ homedir = compat .get_pg_home_directory ()
278
+ if homedir :
279
+ passfile = homedir / PGPASSFILE
280
+ else :
281
+ passfile = None
282
+ else :
283
+ passfile = pathlib .Path (passfile )
284
+
285
+ if passfile is not None :
286
+ password = _read_password_from_pgpass (
287
+ hosts = auth_hosts , port = port , database = database , user = user ,
288
+ passfile = passfile )
289
+
171
290
addrs = []
172
291
for h in host :
173
292
if h .startswith ('/' ):
@@ -206,8 +325,9 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user,
206
325
return addrs , params
207
326
208
327
209
- def _parse_connect_arguments (* , dsn , host , port , user , password , database ,
210
- timeout , command_timeout , statement_cache_size ,
328
+ def _parse_connect_arguments (* , dsn , host , port , user , password , passfile ,
329
+ database , timeout , command_timeout ,
330
+ statement_cache_size ,
211
331
max_cached_statement_lifetime ,
212
332
max_cacheable_statement_size ,
213
333
ssl , server_settings ):
@@ -237,7 +357,7 @@ def _parse_connect_arguments(*, dsn, host, port, user, password, database,
237
357
238
358
addrs , params = _parse_connect_dsn_and_args (
239
359
dsn = dsn , host = host , port = port , user = user ,
240
- password = password , ssl = ssl ,
360
+ password = password , passfile = passfile , ssl = ssl ,
241
361
database = database , connect_timeout = timeout ,
242
362
server_settings = server_settings )
243
363
0 commit comments