/* ------------------------------------------------------------------------- * BKG NTRIP Client * ------------------------------------------------------------------------- * * Class: bncCredentials * * Purpose: Passwords from credentials files (netrc or BNC format) * instead of the configuration file * * Created: 31-Oct-2026 * * Changes: * * -----------------------------------------------------------------------*/ #include #include #include #include #include #include #include #include "bnccredentials.h" #include "bnccore.h" #include "bncsettings.h" #include "bncutils.h" namespace { struct t_entry { QString host; int port; // 0: any port bool hasUser; QString user; QString password; }; struct t_file { t_file() : exists(false), size(-1), refused(false) {} bool exists; QDateTime modified; qint64 size; bool refused; // readable for others QList entries; }; QMutex _mutex; QMap _files; QSet _reported; // Log a message once per key void message(const QString& key, const QString& msg) { if (!_reported.contains(key)) { _reported.insert(key); BNC_CORE->slotMessage(msg.toLatin1(), true); } } // Split a line into tokens: white space separated, "..." quoted (with \" // and \\), '#' outside quotes starts a comment QStringList tokenize(const QString& line) { QStringList tokens; int ii = 0; while (ii < line.size()) { if (line[ii].isSpace()) { ++ii; continue; } if (line[ii] == '#') { break; } QString token; if (line[ii] == '"') { for (++ii; ii < line.size() && line[ii] != '"'; ++ii) { if (line[ii] == '\\' && ii + 1 < line.size()) { ++ii; } token += line[ii]; } ++ii; // closing quote } else { for (; ii < line.size() && !line[ii].isSpace(); ++ii) { token += line[ii]; } } tokens << token; } return tokens; } // "host" or "host:port" void hostAndPort(const QString& name, t_entry& entry) { entry.host = name; entry.port = 0; int ic = name.lastIndexOf(':'); if (ic > 0) { bool ok; int port = name.mid(ic + 1).toInt(&ok); if (ok && port > 0) { entry.host = name.left(ic); entry.port = port; } } } void parseNetrc(const QList& lines, t_file& file) { t_entry* entry = 0; bool isDefault = false; for (int iLine = 0; iLine < lines.size(); iLine++) { const QStringList& tokens = lines[iLine]; for (int ii = 0; ii < tokens.size(); ii++) { const QString& key = tokens[ii]; QString value = (ii + 1 < tokens.size()) ? tokens[ii + 1] : QString(); if (key == "machine") { file.entries.append(t_entry()); entry = &file.entries.last(); entry->hasUser = false; hostAndPort(value, *entry); isDefault = false; ++ii; } else if (key == "default") { // for all hosts: never used by BNC entry = 0; isDefault = true; } else if (key == "login" || key == "password" || key == "account") { if (entry && !isDefault) { if (key == "login") { entry->hasUser = true; entry->user = value; } else if (key == "password") { entry->password = value; } } ++ii; } else if (key == "macdef") { // macro definition up to an empty line while (iLine + 1 < lines.size() && !lines[iLine + 1].isEmpty()) { ++iLine; } break; } } } } void parseBnc(const QString& fileName, const QList& lines, const QList& lineNumbers, t_file& file) { for (int iLine = 0; iLine < lines.size(); iLine++) { const QStringList& tokens = lines[iLine]; if (tokens.isEmpty()) { continue; } // , or for any port QStringList hlp = tokens; if (hlp.size() == 3) { hlp.insert(1, "*"); } bool ok = hlp.size() == 4; t_entry entry; entry.host = hlp.value(0); if (hlp.value(1) == "*") { entry.port = 0; } else { bool okPort; entry.port = hlp.value(1).toInt(&okPort); ok = ok && okPort && entry.port > 0; } entry.hasUser = hlp.value(2) != "-"; entry.user = entry.hasUser ? hlp.value(2) : QString(); entry.password = hlp.value(3); if (ok) { file.entries.append(entry); } else { message(fileName + QString(":%1").arg(lineNumbers[iLine]), QString("Credentials file %1, line %2: not [] , ignored") .arg(fileName).arg(lineNumbers[iLine])); } } } // Entries of a file, read again if it changed const t_file& readFile(const QString& fileName) { t_file& file = _files[fileName]; QFileInfo info(fileName); if (info.exists() == file.exists && (!file.exists || (info.lastModified() == file.modified && info.size() == file.size))) { return file; } file = t_file(); file.exists = info.exists(); if (!file.exists) { return file; } file.modified = info.lastModified(); file.size = info.size(); #ifndef Q_OS_WIN if (info.permissions() & (QFile::ReadGroup | QFile::ReadOther)) { file.refused = true; message("perm:" + fileName, QString("Credentials file %1 is readable for other users, not used (chmod 600)").arg(fileName)); return file; } #endif QFile inp(fileName); if (!inp.open(QIODevice::ReadOnly | QIODevice::Text)) { message("open:" + fileName, QString("Cannot read credentials file %1").arg(fileName)); return file; } QList lines; QList lineNumbers; int lineNumber = 0; while (!inp.atEnd()) { QString line = QString::fromUtf8(inp.readLine()); ++lineNumber; QStringList tokens = tokenize(line); if (tokens.isEmpty() && line.trimmed().startsWith('#')) { continue; // comment line, not the end of a netrc macro } lines << tokens; lineNumbers << lineNumber; } // netrc, if the first token is a netrc keyword QString first; for (int ii = 0; ii < lines.size() && first.isEmpty(); ii++) { first = lines[ii].value(0); } if (first == "machine" || first == "default" || first == "macdef") { parseNetrc(lines, file); } else { parseBnc(fileName, lines, lineNumbers, file); } return file; } bool sameUser(const t_entry& entry, const QString& user) { return user.isEmpty() ? (!entry.hasUser || entry.user.isEmpty()) : (entry.hasUser && entry.user == user); } } // namespace // Credentials files: option 'credentialsFile' (comma separated), default // $NETRC or ~/.netrc (Windows: %HOME%\_netrc) //////////////////////////////////////////////////////////////////////////// QStringList bncCredentials::fileNames() { bncSettings settings; QStringList names; QStringList hlp = settings.value("credentialsFile").toString().split(',', Qt::SkipEmptyParts); for (int ii = 0; ii < hlp.size(); ii++) { QString name = hlp[ii].trimmed(); expandEnvVar(name); if (name.startsWith("~/")) { name = QDir::homePath() + name.mid(1); } if (!name.isEmpty()) { names << name; } } if (names.isEmpty()) { QString netrc = QString::fromLocal8Bit(qgetenv("NETRC")); if (netrc.isEmpty()) { #ifdef Q_OS_WIN netrc = QDir::homePath() + "/_netrc"; #else netrc = QDir::homePath() + "/.netrc"; #endif } names << netrc; } return names; } // Password of user at host:port //////////////////////////////////////////////////////////////////////////// bool bncCredentials::password(const QString& host, int port, const QString& user, QString& password) { QMutexLocker locker(&_mutex); QStringList names = fileNames(); for (int iFile = 0; iFile < names.size(); iFile++) { const t_file& file = readFile(names[iFile]); if (file.refused) { continue; } // exact port first, then entries for any port for (int pass = 0; pass < 2; pass++) { for (int ii = 0; ii < file.entries.size(); ii++) { const t_entry& entry = file.entries[ii]; if (entry.host.compare(host, Qt::CaseInsensitive) == 0 && (pass == 0 ? (port > 0 && entry.port == port) : entry.port == 0) && sameUser(entry, user)) { password = entry.password; return true; } } } } return false; } // Stream URL //////////////////////////////////////////////////////////////////////////// void bncCredentials::complete(QUrl& url) { if (url.userName().isEmpty()) { return; // anonymous } QString user = QUrl::fromPercentEncoding(url.userName().toLatin1()); QString pass = url.password(); complete(url.host(), url.port(0), user, pass, false, true); if (url.password().isEmpty() && !pass.isEmpty()) { url.setPassword(pass, QUrl::DecodedMode); } } // Upload or sourcetable //////////////////////////////////////////////////////////////////////////// void bncCredentials::complete(const QString& host, int port, const QString& user, QString& password, bool anyUser, bool hint) { QString account = (user.isEmpty() ? QString() : user + "@") + host + QString(":%1").arg(port); if (!password.isEmpty()) { if (hint) { QMutexLocker locker(&_mutex); message("config:" + account, QString("%1: password in the configuration file, it can be moved to a credentials file " "(option 'credentialsFile', default ~/.netrc)").arg(account)); } return; } if (user.isEmpty() && !anyUser) { return; } if (!bncCredentials::password(host, port, user, password)) { QMutexLocker locker(&_mutex); message("none:" + account, QString("%1: no password in the configuration file nor in %2") .arg(account).arg(fileNames().join(", "))); } }