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

Last change on this file since 11153 was 11153, checked in by stuerze, 12 days ago

Credentials files are implemented, with existing configs keep working unchanged

File size: 9.7 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 bool ok = tokens.size() == 4;
158 t_entry entry;
159 entry.host = tokens.value(0);
160 if (tokens.value(1) == "*") {
161 entry.port = 0;
162 }
163 else {
164 bool okPort;
165 entry.port = tokens.value(1).toInt(&okPort);
166 ok = ok && okPort && entry.port > 0;
167 }
168 entry.hasUser = tokens.value(2) != "-";
169 entry.user = entry.hasUser ? tokens.value(2) : QString();
170 entry.password = tokens.value(3);
171 if (ok) {
172 file.entries.append(entry);
173 }
174 else {
175 message(fileName + QString(":%1").arg(lineNumbers[iLine]),
176 QString("Credentials file %1, line %2: not <host> <port> <user> <password>, ignored")
177 .arg(fileName).arg(lineNumbers[iLine]));
178 }
179 }
180}
181
182// Entries of a file, read again if it changed
183const t_file& readFile(const QString& fileName) {
184 t_file& file = _files[fileName];
185 QFileInfo info(fileName);
186 if (info.exists() == file.exists && (!file.exists ||
187 (info.lastModified() == file.modified && info.size() == file.size))) {
188 return file;
189 }
190 file = t_file();
191 file.exists = info.exists();
192 if (!file.exists) {
193 return file;
194 }
195 file.modified = info.lastModified();
196 file.size = info.size();
197
198#ifndef Q_OS_WIN
199 if (info.permissions() & (QFile::ReadGroup | QFile::ReadOther)) {
200 file.refused = true;
201 message("perm:" + fileName,
202 QString("Credentials file %1 is readable for other users, not used (chmod 600)").arg(fileName));
203 return file;
204 }
205#endif
206
207 QFile inp(fileName);
208 if (!inp.open(QIODevice::ReadOnly | QIODevice::Text)) {
209 message("open:" + fileName, QString("Cannot read credentials file %1").arg(fileName));
210 return file;
211 }
212 QList<QStringList> lines;
213 QList<int> lineNumbers;
214 int lineNumber = 0;
215 while (!inp.atEnd()) {
216 QString line = QString::fromUtf8(inp.readLine());
217 ++lineNumber;
218 QStringList tokens = tokenize(line);
219 if (tokens.isEmpty() && line.trimmed().startsWith('#')) {
220 continue; // comment line, not the end of a netrc macro
221 }
222 lines << tokens;
223 lineNumbers << lineNumber;
224 }
225
226 // netrc, if the first token is a netrc keyword
227 QString first;
228 for (int ii = 0; ii < lines.size() && first.isEmpty(); ii++) {
229 first = lines[ii].value(0);
230 }
231 if (first == "machine" || first == "default" || first == "macdef") {
232 parseNetrc(lines, file);
233 }
234 else {
235 parseBnc(fileName, lines, lineNumbers, file);
236 }
237 return file;
238}
239
240bool sameUser(const t_entry& entry, const QString& user) {
241 return user.isEmpty() ? (!entry.hasUser || entry.user.isEmpty())
242 : (entry.hasUser && entry.user == user);
243}
244
245} // namespace
246
247// Credentials files: option 'credentialsFile' (comma separated), default
248// $NETRC or ~/.netrc (Windows: %HOME%\_netrc)
249////////////////////////////////////////////////////////////////////////////
250QStringList bncCredentials::fileNames() {
251 bncSettings settings;
252 QStringList names;
253 QStringList hlp = settings.value("credentialsFile").toString().split(',', Qt::SkipEmptyParts);
254 for (int ii = 0; ii < hlp.size(); ii++) {
255 QString name = hlp[ii].trimmed();
256 expandEnvVar(name);
257 if (name.startsWith("~/")) {
258 name = QDir::homePath() + name.mid(1);
259 }
260 if (!name.isEmpty()) {
261 names << name;
262 }
263 }
264 if (names.isEmpty()) {
265 QString netrc = QString::fromLocal8Bit(qgetenv("NETRC"));
266 if (netrc.isEmpty()) {
267#ifdef Q_OS_WIN
268 netrc = QDir::homePath() + "/_netrc";
269#else
270 netrc = QDir::homePath() + "/.netrc";
271#endif
272 }
273 names << netrc;
274 }
275 return names;
276}
277
278// Password of user at host:port
279////////////////////////////////////////////////////////////////////////////
280bool bncCredentials::password(const QString& host, int port, const QString& user,
281 QString& password) {
282 QMutexLocker locker(&_mutex);
283 QStringList names = fileNames();
284 for (int iFile = 0; iFile < names.size(); iFile++) {
285 const t_file& file = readFile(names[iFile]);
286 if (file.refused) {
287 continue;
288 }
289 // exact port first, then entries for any port
290 for (int pass = 0; pass < 2; pass++) {
291 for (int ii = 0; ii < file.entries.size(); ii++) {
292 const t_entry& entry = file.entries[ii];
293 if (entry.host.compare(host, Qt::CaseInsensitive) == 0 &&
294 (pass == 0 ? (port > 0 && entry.port == port) : entry.port == 0) &&
295 sameUser(entry, user)) {
296 password = entry.password;
297 return true;
298 }
299 }
300 }
301 }
302 return false;
303}
304
305// Stream URL
306////////////////////////////////////////////////////////////////////////////
307void bncCredentials::complete(QUrl& url) {
308 if (url.userName().isEmpty()) {
309 return; // anonymous
310 }
311 QString user = QUrl::fromPercentEncoding(url.userName().toLatin1());
312 QString pass = url.password();
313 complete(url.host(), url.port(0), user, pass, false, true);
314 if (url.password().isEmpty() && !pass.isEmpty()) {
315 url.setPassword(pass, QUrl::DecodedMode);
316 }
317}
318
319// Upload or sourcetable
320////////////////////////////////////////////////////////////////////////////
321void bncCredentials::complete(const QString& host, int port, const QString& user,
322 QString& password, bool anyUser, bool hint) {
323 QString account = (user.isEmpty() ? QString() : user + "@") + host + QString(":%1").arg(port);
324 if (!password.isEmpty()) {
325 if (hint) {
326 QMutexLocker locker(&_mutex);
327 message("config:" + account,
328 QString("%1: password in the configuration file, it can be moved to a credentials file "
329 "(option 'credentialsFile', default ~/.netrc)").arg(account));
330 }
331 return;
332 }
333 if (user.isEmpty() && !anyUser) {
334 return;
335 }
336 if (!bncCredentials::password(host, port, user, password)) {
337 QMutexLocker locker(&_mutex);
338 message("none:" + account, QString("%1: no password in the configuration file nor in %2")
339 .arg(account).arg(fileNames().join(", ")));
340 }
341}
Note: See TracBrowser for help on using the repository browser.