source: ntrip/trunk/BNC/src/bnccredentials.cpp

Last change on this file was 11160, checked in by stuerze, 3 days ago

screenshots are updated

File size: 9.9 KB
Line 
1/* -------------------------------------------------------------------------
2 * BKG NTRIP Client
3 * -------------------------------------------------------------------------
4 *
5 * Class: bncCredentials
6 *
7 * Purpose: Passwords from credentials files (netrc or BNC format)
8 * instead of the configuration file
9 *
10 * Created: 31-Oct-2026
11 *
12 * Changes:
13 *
14 * -----------------------------------------------------------------------*/
15
16#include <QDateTime>
17#include <QDir>
18#include <QFile>
19#include <QFileInfo>
20#include <QMap>
21#include <QMutex>
22#include <QSet>
23
24#include "bnccredentials.h"
25#include "bnccore.h"
26#include "bncsettings.h"
27#include "bncutils.h"
28
29namespace {
30
31struct t_entry {
32 QString host;
33 int port; // 0: any port
34 bool hasUser;
35 QString user;
36 QString password;
37};
38
39struct t_file {
40 t_file() : exists(false), size(-1), refused(false) {}
41 bool exists;
42 QDateTime modified;
43 qint64 size;
44 bool refused; // readable for others
45 QList<t_entry> entries;
46};
47
48QMutex _mutex;
49QMap<QString, t_file> _files;
50QSet<QString> _reported;
51
52// Log a message once per key
53void message(const QString& key, const QString& msg) {
54 if (!_reported.contains(key)) {
55 _reported.insert(key);
56 BNC_CORE->slotMessage(msg.toLatin1(), true);
57 }
58}
59
60// Split a line into tokens: white space separated, "..." quoted (with \"
61// and \\), '#' outside quotes starts a comment
62QStringList tokenize(const QString& line) {
63 QStringList tokens;
64 int ii = 0;
65 while (ii < line.size()) {
66 if (line[ii].isSpace()) {
67 ++ii;
68 continue;
69 }
70 if (line[ii] == '#') {
71 break;
72 }
73 QString token;
74 if (line[ii] == '"') {
75 for (++ii; ii < line.size() && line[ii] != '"'; ++ii) {
76 if (line[ii] == '\\' && ii + 1 < line.size()) {
77 ++ii;
78 }
79 token += line[ii];
80 }
81 ++ii; // closing quote
82 }
83 else {
84 for (; ii < line.size() && !line[ii].isSpace(); ++ii) {
85 token += line[ii];
86 }
87 }
88 tokens << token;
89 }
90 return tokens;
91}
92
93// "host" or "host:port"
94void hostAndPort(const QString& name, t_entry& entry) {
95 entry.host = name;
96 entry.port = 0;
97 int ic = name.lastIndexOf(':');
98 if (ic > 0) {
99 bool ok;
100 int port = name.mid(ic + 1).toInt(&ok);
101 if (ok && port > 0) {
102 entry.host = name.left(ic);
103 entry.port = port;
104 }
105 }
106}
107
108void parseNetrc(const QList<QStringList>& lines, t_file& file) {
109 t_entry* entry = 0;
110 bool isDefault = false;
111 for (int iLine = 0; iLine < lines.size(); iLine++) {
112 const QStringList& tokens = lines[iLine];
113 for (int ii = 0; ii < tokens.size(); ii++) {
114 const QString& key = tokens[ii];
115 QString value = (ii + 1 < tokens.size()) ? tokens[ii + 1] : QString();
116 if (key == "machine") {
117 file.entries.append(t_entry());
118 entry = &file.entries.last();
119 entry->hasUser = false;
120 hostAndPort(value, *entry);
121 isDefault = false;
122 ++ii;
123 }
124 else if (key == "default") { // for all hosts: never used by BNC
125 entry = 0;
126 isDefault = true;
127 }
128 else if (key == "login" || key == "password" || key == "account") {
129 if (entry && !isDefault) {
130 if (key == "login") {
131 entry->hasUser = true;
132 entry->user = value;
133 }
134 else if (key == "password") {
135 entry->password = value;
136 }
137 }
138 ++ii;
139 }
140 else if (key == "macdef") { // macro definition up to an empty line
141 while (iLine + 1 < lines.size() && !lines[iLine + 1].isEmpty()) {
142 ++iLine;
143 }
144 break;
145 }
146 }
147 }
148}
149
150void parseBnc(const QString& fileName, const QList<QStringList>& lines,
151 const QList<int>& lineNumbers, t_file& file) {
152 for (int iLine = 0; iLine < lines.size(); iLine++) {
153 const QStringList& tokens = lines[iLine];
154 if (tokens.isEmpty()) {
155 continue;
156 }
157 // <host> <port|*> <user|-> <password>, or <host> <user> <password> for any port
158 QStringList hlp = tokens;
159 if (hlp.size() == 3) {
160 hlp.insert(1, "*");
161 }
162 bool ok = hlp.size() == 4;
163 t_entry entry;
164 entry.host = hlp.value(0);
165 if (hlp.value(1) == "*") {
166 entry.port = 0;
167 }
168 else {
169 bool okPort;
170 entry.port = hlp.value(1).toInt(&okPort);
171 ok = ok && okPort && entry.port > 0;
172 }
173 entry.hasUser = hlp.value(2) != "-";
174 entry.user = entry.hasUser ? hlp.value(2) : QString();
175 entry.password = hlp.value(3);
176 if (ok) {
177 file.entries.append(entry);
178 }
179 else {
180 message(fileName + QString(":%1").arg(lineNumbers[iLine]),
181 QString("Credentials file %1, line %2: not <host> [<port>] <user> <password>, ignored")
182 .arg(fileName).arg(lineNumbers[iLine]));
183 }
184 }
185}
186
187// Entries of a file, read again if it changed
188const t_file& readFile(const QString& fileName) {
189 t_file& file = _files[fileName];
190 QFileInfo info(fileName);
191 if (info.exists() == file.exists && (!file.exists ||
192 (info.lastModified() == file.modified && info.size() == file.size))) {
193 return file;
194 }
195 file = t_file();
196 file.exists = info.exists();
197 if (!file.exists) {
198 return file;
199 }
200 file.modified = info.lastModified();
201 file.size = info.size();
202
203#ifndef Q_OS_WIN
204 if (info.permissions() & (QFile::ReadGroup | QFile::ReadOther)) {
205 file.refused = true;
206 message("perm:" + fileName,
207 QString("Credentials file %1 is readable for other users, not used (chmod 600)").arg(fileName));
208 return file;
209 }
210#endif
211
212 QFile inp(fileName);
213 if (!inp.open(QIODevice::ReadOnly | QIODevice::Text)) {
214 message("open:" + fileName, QString("Cannot read credentials file %1").arg(fileName));
215 return file;
216 }
217 QList<QStringList> lines;
218 QList<int> lineNumbers;
219 int lineNumber = 0;
220 while (!inp.atEnd()) {
221 QString line = QString::fromUtf8(inp.readLine());
222 ++lineNumber;
223 QStringList tokens = tokenize(line);
224 if (tokens.isEmpty() && line.trimmed().startsWith('#')) {
225 continue; // comment line, not the end of a netrc macro
226 }
227 lines << tokens;
228 lineNumbers << lineNumber;
229 }
230
231 // netrc, if the first token is a netrc keyword
232 QString first;
233 for (int ii = 0; ii < lines.size() && first.isEmpty(); ii++) {
234 first = lines[ii].value(0);
235 }
236 if (first == "machine" || first == "default" || first == "macdef") {
237 parseNetrc(lines, file);
238 }
239 else {
240 parseBnc(fileName, lines, lineNumbers, file);
241 }
242 return file;
243}
244
245bool sameUser(const t_entry& entry, const QString& user) {
246 return user.isEmpty() ? (!entry.hasUser || entry.user.isEmpty())
247 : (entry.hasUser && entry.user == user);
248}
249
250} // namespace
251
252// Credentials files: option 'credentialsFile' (comma separated), default
253// $NETRC or ~/.netrc (Windows: %HOME%\_netrc)
254////////////////////////////////////////////////////////////////////////////
255QStringList bncCredentials::fileNames() {
256 bncSettings settings;
257 QStringList names;
258 QStringList hlp = settings.value("credentialsFile").toString().split(',', Qt::SkipEmptyParts);
259 for (int ii = 0; ii < hlp.size(); ii++) {
260 QString name = hlp[ii].trimmed();
261 expandEnvVar(name);
262 if (name.startsWith("~/")) {
263 name = QDir::homePath() + name.mid(1);
264 }
265 if (!name.isEmpty()) {
266 names << name;
267 }
268 }
269 if (names.isEmpty()) {
270 QString netrc = QString::fromLocal8Bit(qgetenv("NETRC"));
271 if (netrc.isEmpty()) {
272#ifdef Q_OS_WIN
273 netrc = QDir::homePath() + "/_netrc";
274#else
275 netrc = QDir::homePath() + "/.netrc";
276#endif
277 }
278 names << netrc;
279 }
280 return names;
281}
282
283// Password of user at host:port
284////////////////////////////////////////////////////////////////////////////
285bool bncCredentials::password(const QString& host, int port, const QString& user,
286 QString& password) {
287 QMutexLocker locker(&_mutex);
288 QStringList names = fileNames();
289 for (int iFile = 0; iFile < names.size(); iFile++) {
290 const t_file& file = readFile(names[iFile]);
291 if (file.refused) {
292 continue;
293 }
294 // exact port first, then entries for any port
295 for (int pass = 0; pass < 2; pass++) {
296 for (int ii = 0; ii < file.entries.size(); ii++) {
297 const t_entry& entry = file.entries[ii];
298 if (entry.host.compare(host, Qt::CaseInsensitive) == 0 &&
299 (pass == 0 ? (port > 0 && entry.port == port) : entry.port == 0) &&
300 sameUser(entry, user)) {
301 password = entry.password;
302 return true;
303 }
304 }
305 }
306 }
307 return false;
308}
309
310// Stream URL
311////////////////////////////////////////////////////////////////////////////
312void bncCredentials::complete(QUrl& url) {
313 if (url.userName().isEmpty()) {
314 return; // anonymous
315 }
316 QString user = QUrl::fromPercentEncoding(url.userName().toLatin1());
317 QString pass = url.password();
318 complete(url.host(), url.port(0), user, pass, false, true);
319 if (url.password().isEmpty() && !pass.isEmpty()) {
320 url.setPassword(pass, QUrl::DecodedMode);
321 }
322}
323
324// Upload or sourcetable
325////////////////////////////////////////////////////////////////////////////
326void bncCredentials::complete(const QString& host, int port, const QString& user,
327 QString& password, bool anyUser, bool hint) {
328 QString account = (user.isEmpty() ? QString() : user + "@") + host + QString(":%1").arg(port);
329 if (!password.isEmpty()) {
330 if (hint) {
331 QMutexLocker locker(&_mutex);
332 message("config:" + account,
333 QString("%1: password in the configuration file, it can be moved to a credentials file "
334 "(option 'credentialsFile', default ~/.netrc)").arg(account));
335 }
336 return;
337 }
338 if (user.isEmpty() && !anyUser) {
339 return;
340 }
341 if (!bncCredentials::password(host, port, user, password)) {
342 QMutexLocker locker(&_mutex);
343 message("none:" + account, QString("%1: no password in the configuration file nor in %2")
344 .arg(account).arg(fileNames().join(", ")));
345 }
346}
Note: See TracBrowser for help on using the repository browser.