mirror of
https://git.jami.net/savoirfairelinux/jami-client-qt.git
synced 2025-07-25 01:45:35 +02:00
619 lines
17 KiB
C++
619 lines
17 KiB
C++
/****************************************************************************
|
|
* Copyright (C) 2014-2024 Savoir-faire Linux Inc. *
|
|
* Author : Emmanuel Lepage Vallee <emmanuel.lepage@savoirfairelinux.com> *
|
|
* Author : Hugo Lefeuvre <hugo.lefeuvre@savoirfairelinux.com> *
|
|
* *
|
|
* This library is free software; you can redistribute it and/or *
|
|
* modify it under the terms of the GNU Lesser General Public *
|
|
* License as published by the Free Software Foundation; either *
|
|
* version 2.1 of the License, or (at your option) any later version. *
|
|
* *
|
|
* This library is distributed in the hope that it will be useful, *
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
|
* Lesser General Public License for more details. *
|
|
* *
|
|
* You should have received a copy of the GNU General Public License *
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
***************************************************************************/
|
|
|
|
#include "uri.h"
|
|
#include <regex>
|
|
|
|
class URIPimpl
|
|
{
|
|
public:
|
|
// Strings associated with SchemeType
|
|
static const std::map<URI::SchemeType, const char*> schemeNames;
|
|
|
|
// String associated with the transport name
|
|
static const std::map<URI::Transport, const char*> transportNames;
|
|
|
|
struct Constants
|
|
{
|
|
constexpr static const char TRANSPORT[] = "transport";
|
|
constexpr static const char TAG[] = "tag";
|
|
};
|
|
|
|
URIPimpl(const URI& uri);
|
|
URIPimpl& operator=(const URIPimpl&);
|
|
|
|
// Attributes
|
|
const URI& linked;
|
|
|
|
QString m_ExtHostname;
|
|
QString m_Scheme;
|
|
QString m_Userinfo;
|
|
QStringList m_lAttributes;
|
|
QString m_Stripped;
|
|
QString m_Hostname2;
|
|
QByteArray m_Tag;
|
|
int m_Port = -1;
|
|
|
|
URI::SchemeType m_HeaderType = URI::SchemeType::NONE;
|
|
URI::Transport m_Transport = URI::Transport::NOT_SET;
|
|
URI::ProtocolHint m_ProtocolHint = URI::ProtocolHint::UNRECOGNIZED;
|
|
|
|
bool m_hasChevrons {false};
|
|
bool m_Parsed {false};
|
|
bool m_HasAt {false};
|
|
bool m_HintParsed {false};
|
|
bool m_IsHNParsed {false};
|
|
|
|
// Helpers
|
|
URI::Transport nameToTransport(const QByteArray& name);
|
|
static QString strip(const QString& uri, URI::SchemeType& schemeType, QString& scheme);
|
|
static bool checkIp(const QString& str, bool& isHash, const URI::SchemeType& scheme);
|
|
void parseAttribute(const QByteArray& extHn, const int start, const int pos);
|
|
void parseHostname();
|
|
void parse();
|
|
};
|
|
|
|
constexpr const char URIPimpl::Constants::TRANSPORT[];
|
|
constexpr const char URIPimpl::Constants::TAG[];
|
|
|
|
const std::map<URI::Transport, const char*> URIPimpl::transportNames
|
|
= {{URI::Transport::NOT_SET, "NOT_SET"},
|
|
{URI::Transport::TLS, "TLS"},
|
|
{URI::Transport::tls, "tls"},
|
|
{URI::Transport::TCP, "TCP"},
|
|
{URI::Transport::tcp, "tcp"},
|
|
{URI::Transport::UDP, "UDP"},
|
|
{URI::Transport::udp, "udp"},
|
|
{URI::Transport::SCTP, "SCTP"},
|
|
{URI::Transport::sctp, "sctp"},
|
|
{URI::Transport::DTLS, "DTLS"},
|
|
{URI::Transport::dtls, "dtls"}};
|
|
|
|
const std::map<URI::SchemeType, const char*> URIPimpl::schemeNames
|
|
= {{URI::SchemeType::NONE, ""},
|
|
{URI::SchemeType::SIP, "sip:"},
|
|
{URI::SchemeType::SIPS, "sips:"},
|
|
{URI::SchemeType::RING, "ring:"}};
|
|
|
|
URIPimpl::URIPimpl(const URI& uri)
|
|
: linked(uri)
|
|
{}
|
|
|
|
URIPimpl&
|
|
URIPimpl::operator=(const URIPimpl& other)
|
|
{
|
|
m_Parsed = other.m_Parsed;
|
|
m_HintParsed = other.m_HintParsed;
|
|
m_ExtHostname = other.m_ExtHostname;
|
|
m_HasAt = other.m_HasAt;
|
|
m_Scheme = other.m_Scheme;
|
|
m_ProtocolHint = other.m_ProtocolHint;
|
|
m_HeaderType = other.m_HeaderType;
|
|
m_Userinfo = other.m_Userinfo;
|
|
m_Stripped = other.m_Stripped;
|
|
m_IsHNParsed = other.m_IsHNParsed;
|
|
m_Port = other.m_Port;
|
|
m_Transport = other.m_Transport;
|
|
m_Tag = other.m_Tag;
|
|
m_hasChevrons = other.m_hasChevrons;
|
|
m_Hostname2 = other.m_Hostname2;
|
|
m_lAttributes = other.m_lAttributes;
|
|
return *this;
|
|
}
|
|
|
|
URI::URI()
|
|
: QString()
|
|
, pimpl_(std::make_unique<URIPimpl>(*this))
|
|
{}
|
|
|
|
URI::URI(const QString& uri)
|
|
: URI()
|
|
{
|
|
QString simplified = uri.simplified().remove(' ').remove('<').remove('>');
|
|
pimpl_->m_Stripped = URIPimpl::strip(simplified, pimpl_->m_HeaderType, pimpl_->m_Scheme);
|
|
(*static_cast<QString*>(this)) = pimpl_->m_Stripped;
|
|
}
|
|
|
|
URI::URI(const URI& other)
|
|
: URI()
|
|
{
|
|
*pimpl_ = *other.pimpl_;
|
|
(*static_cast<QString*>(this)) = pimpl_->m_Stripped;
|
|
}
|
|
|
|
URI&
|
|
URI::operator=(const URI& other)
|
|
{
|
|
if (this != &other) {
|
|
*pimpl_ = *other.pimpl_;
|
|
(*static_cast<QString*>(this)) = pimpl_->m_Stripped;
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
URI::~URI() {}
|
|
|
|
/**
|
|
* Strip out scheme from the URI
|
|
*/
|
|
QString
|
|
URIPimpl::strip(const QString& uri, URI::SchemeType& schemeType, QString& scheme)
|
|
{
|
|
if (uri.isEmpty())
|
|
return {};
|
|
std::regex uri_regex = std::regex("(^sip:)|(^sips:)|(^ring:)|(^jami:)");
|
|
std::string uri_to_match = uri.toStdString();
|
|
std::smatch match;
|
|
|
|
if (std::regex_search(uri_to_match, match, uri_regex)) {
|
|
if (match.ready()) {
|
|
scheme = match.str(0).c_str();
|
|
}
|
|
}
|
|
|
|
schemeType = URI::SchemeType::UNRECOGNIZED;
|
|
auto it = std::find_if(schemeNames.begin(), schemeNames.end(), [&scheme](auto& it) {
|
|
if (std::string(it.second) == "ring:" && scheme == "jami:")
|
|
return true;
|
|
return it.second == scheme;
|
|
});
|
|
|
|
if (it != URIPimpl::schemeNames.end())
|
|
schemeType = it->first;
|
|
if (schemeType == URI::SchemeType::NONE) {
|
|
// no match, check if it includes x..x:..: format
|
|
std::regex uri_regex_unrecognized = std::regex("[a-zA-Z][a-zA-Z0-9+.-]*:");
|
|
std::string uri_to_match_unrecognized = uri_to_match;
|
|
std::smatch match_unrecognized;
|
|
|
|
if (std::regex_search(uri_to_match_unrecognized,
|
|
match_unrecognized,
|
|
uri_regex_unrecognized)) {
|
|
if (match_unrecognized.ready()) {
|
|
std::string scheme_unrecognized = match_unrecognized.str(0).c_str();
|
|
if (!scheme_unrecognized.empty()) {
|
|
// match x..x:..: format
|
|
schemeType = URI::SchemeType::UNRECOGNIZED;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return uri.mid(scheme.size(), uri.size());
|
|
}
|
|
|
|
/**
|
|
* Return the domain of the URI
|
|
*
|
|
* For example, example.com in <sip:12345@example.com>
|
|
*/
|
|
QString
|
|
URI::hostname() const
|
|
{
|
|
if (!pimpl_->m_Parsed)
|
|
pimpl_->parse();
|
|
return pimpl_->m_ExtHostname;
|
|
}
|
|
|
|
/**
|
|
* Check if the URI has an hostname
|
|
*
|
|
* This will return true if there is something between '@' and ';' (or an end of line)
|
|
*/
|
|
bool
|
|
URI::hasHostname() const
|
|
{
|
|
return !hostname().isEmpty();
|
|
}
|
|
|
|
/**
|
|
* If hasHostname() is true, this does a second parsing of the hostname to
|
|
* extract the port.
|
|
*/
|
|
bool
|
|
URI::hasPort() const
|
|
{
|
|
return port() != -1;
|
|
}
|
|
|
|
/**
|
|
* Return the port, -1 is none is set
|
|
*/
|
|
int
|
|
URI::port() const
|
|
{
|
|
if (!pimpl_->m_IsHNParsed) {
|
|
pimpl_->parseHostname();
|
|
}
|
|
return pimpl_->m_Port;
|
|
}
|
|
|
|
/**
|
|
* Return the URI SchemeType
|
|
*/
|
|
URI::SchemeType
|
|
URI::schemeType() const
|
|
{
|
|
if (!pimpl_->m_Parsed)
|
|
const_cast<URI*>(this)->pimpl_->parse();
|
|
return pimpl_->m_HeaderType;
|
|
}
|
|
|
|
/**
|
|
* "Fast" Ipv4 and Ipv6 check, accept 999.999.999.999, :::::::::FF and other
|
|
* atrocities, but at least perform a O(N) ish check and validate the hash
|
|
*
|
|
* @param str an uservalue (faster the scheme and before the "at" sign)
|
|
* @param [out] isHash if the content is pure hexadecimal ASCII
|
|
*/
|
|
bool
|
|
URIPimpl::checkIp(const QString& str, bool& isHash, const URI::SchemeType& scheme)
|
|
{
|
|
const QByteArray raw = str.toLatin1();
|
|
int max = str.size();
|
|
|
|
if (max < 3 || max > 45 || (!isHash && scheme == URI::SchemeType::RING))
|
|
return false;
|
|
|
|
uchar dc(0), sc(0), i(0), d(0), hx(1);
|
|
|
|
while (i < max) {
|
|
switch (raw[i]) {
|
|
case '.':
|
|
isHash = false;
|
|
d = 0;
|
|
dc++;
|
|
break;
|
|
case '0':
|
|
case '1':
|
|
case '2':
|
|
case '3':
|
|
case '4':
|
|
case '5':
|
|
case '6':
|
|
case '7':
|
|
case '8':
|
|
case '9':
|
|
if (++d > 3 && dc)
|
|
return false;
|
|
break;
|
|
case ':':
|
|
isHash = false;
|
|
sc++;
|
|
// No break
|
|
[[fallthrough]];
|
|
case 'A':
|
|
case 'B':
|
|
case 'C':
|
|
case 'D':
|
|
case 'E':
|
|
case 'F':
|
|
case 'a':
|
|
case 'b':
|
|
case 'c':
|
|
case 'd':
|
|
case 'e':
|
|
case 'f':
|
|
hx = 0;
|
|
break;
|
|
default:
|
|
isHash = false;
|
|
return false;
|
|
}
|
|
i++;
|
|
}
|
|
return (hx && dc == 3 && d < 4) ^ (sc > 1 && dc == 0);
|
|
}
|
|
|
|
/**
|
|
* This method return an hint to guess the protocol that could be used to call
|
|
* this URI. It is a quick guess, not something that should be trusted
|
|
*
|
|
* @warning, this method is O(N) when called for the first time on an URI
|
|
*/
|
|
URI::ProtocolHint
|
|
URI::protocolHint() const
|
|
{
|
|
if (!pimpl_->m_Parsed)
|
|
pimpl_->parse();
|
|
|
|
if (!pimpl_->m_HintParsed) {
|
|
bool isHash = pimpl_->m_Userinfo.size() == 40;
|
|
|
|
URI::ProtocolHint hint;
|
|
|
|
// Step 1: Check IP
|
|
if (URIPimpl::checkIp(pimpl_->m_Userinfo, isHash, pimpl_->m_HeaderType)) {
|
|
hint = URI::ProtocolHint::IP;
|
|
}
|
|
// Step 2: Check RING hash
|
|
else if (isHash) {
|
|
hint = URI::ProtocolHint::RING;
|
|
}
|
|
// Step 3: Not a hash but it begins with ring:. This is a username.
|
|
else if (pimpl_->m_HeaderType == URI::SchemeType::RING) {
|
|
hint = URI::ProtocolHint::RING_USERNAME;
|
|
}
|
|
// Step 4: Check for SIP URIs
|
|
else if (pimpl_->m_HeaderType == URI::SchemeType::SIP) {
|
|
// Step 4.1: Check for SIP URI with hostname
|
|
if (pimpl_->m_HasAt) {
|
|
hint = URI::ProtocolHint::SIP_HOST;
|
|
}
|
|
// Step 4.2: Assume SIP URI without hostname
|
|
else {
|
|
hint = URI::ProtocolHint::SIP_OTHER;
|
|
}
|
|
}
|
|
// Step 5: UNRECOGNIZED
|
|
else {
|
|
hint = URI::ProtocolHint::UNRECOGNIZED;
|
|
}
|
|
|
|
pimpl_->m_ProtocolHint = hint;
|
|
pimpl_->m_HintParsed = true;
|
|
}
|
|
return pimpl_->m_ProtocolHint;
|
|
}
|
|
|
|
// Convert the transport name to a string
|
|
URI::Transport
|
|
URIPimpl::nameToTransport(const QByteArray& name)
|
|
{
|
|
auto it = std::find_if(transportNames.begin(), transportNames.end(), [&name](auto& entry) {
|
|
return entry.second == name;
|
|
});
|
|
|
|
if (it != transportNames.end()) {
|
|
return it->first;
|
|
} else {
|
|
return URI::Transport::NOT_SET;
|
|
}
|
|
}
|
|
|
|
// Keep a cache of the values to avoid re-parsing them
|
|
void
|
|
URIPimpl::parse()
|
|
{
|
|
// FIXME the indexOf is done twice, the second time could be avoided
|
|
if (linked.indexOf('@') != -1) {
|
|
const QStringList split = linked.split('@');
|
|
m_HasAt = true;
|
|
m_ExtHostname = split[1];
|
|
m_Userinfo = split[0];
|
|
m_Parsed = true;
|
|
} else {
|
|
m_Userinfo = (linked);
|
|
}
|
|
}
|
|
|
|
void
|
|
URIPimpl::parseAttribute(const QByteArray& extHn, const int start, const int pos)
|
|
{
|
|
const QList<QByteArray> parts = extHn.mid(start + 1, pos - start).split('=');
|
|
|
|
if (parts.size() == 2) {
|
|
if (parts[0].toLower() == Constants::TRANSPORT) {
|
|
m_Transport = nameToTransport(parts[1]);
|
|
} else if (parts[0].toLower() == Constants::TAG) {
|
|
m_Tag = parts[1];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Extract the hostname, port and attributes
|
|
void
|
|
URIPimpl::parseHostname()
|
|
{
|
|
if (!m_Parsed)
|
|
parse();
|
|
|
|
const QByteArray extHn = linked.hostname().toLatin1();
|
|
int length(extHn.size()), start(0);
|
|
bool inAttributes = false;
|
|
|
|
URI::Section section = URI::Section::HOSTNAME;
|
|
|
|
// in case no port, attributes, etc are provided
|
|
m_Hostname2 = linked.hostname();
|
|
|
|
for (int i = 0; i < length; i++) {
|
|
const char c = extHn[i];
|
|
switch (c) {
|
|
case ':': // Begin port
|
|
if (section == URI::Section::HOSTNAME) {
|
|
m_Hostname2 = extHn.mid(start, i);
|
|
start = i;
|
|
section = URI::Section::PORT;
|
|
}
|
|
break;
|
|
case ';': // Begin attributes
|
|
if (inAttributes) {
|
|
parseAttribute(extHn, start, i);
|
|
} else {
|
|
if (section == URI::Section::HOSTNAME) {
|
|
m_Hostname2 = extHn.mid(start + 1, i - start);
|
|
} else if (section == URI::Section::PORT) {
|
|
m_Port = extHn.mid(start + 1, i - start - 1).toInt();
|
|
}
|
|
inAttributes = true;
|
|
}
|
|
|
|
start = i;
|
|
break;
|
|
case '#': // Begin fragments
|
|
// TODO handle fragments to comply to the RFC
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Get the remaining attribute
|
|
parseAttribute(extHn, start, length - 1);
|
|
m_IsHNParsed = true;
|
|
}
|
|
|
|
/**
|
|
* Extract the user info field from the URI
|
|
*
|
|
* For example, "123" in sip:123@myserver.net
|
|
*/
|
|
QString
|
|
URI::userinfo() const
|
|
{
|
|
if (!pimpl_->m_Parsed)
|
|
pimpl_->parse();
|
|
return pimpl_->m_Userinfo;
|
|
}
|
|
|
|
void
|
|
URI::setUserinfo(const QString& userinfo)
|
|
{
|
|
pimpl_->m_Userinfo = userinfo;
|
|
}
|
|
|
|
void
|
|
URI::setHostname(const QString& hostname)
|
|
{
|
|
pimpl_->m_ExtHostname = hostname;
|
|
}
|
|
|
|
void
|
|
URI::setPort(const QString& port)
|
|
{
|
|
try {
|
|
pimpl_->m_Port = port.toInt();
|
|
} catch (...) {
|
|
qWarning() << "Can't convert port to integer";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sometime, some metadata can be used to deduce the scheme even if it wasn't
|
|
* originally known.
|
|
*/
|
|
void
|
|
URI::setSchemeType(SchemeType t)
|
|
{
|
|
pimpl_->m_HeaderType = t;
|
|
pimpl_->m_Scheme = URIPimpl::schemeNames.at(t);
|
|
}
|
|
|
|
/**
|
|
* Generate a new URI formatted with the sections passed in `sections`
|
|
*
|
|
* It is kept as a QString to avoid the URI class to start reformatting
|
|
* it right away.
|
|
*/
|
|
QString
|
|
URI::format(FlagPack<URI::Section> sections) const
|
|
{
|
|
if (!pimpl_->m_IsHNParsed) {
|
|
pimpl_->parseHostname();
|
|
}
|
|
|
|
auto header_type = pimpl_->m_HeaderType;
|
|
if (header_type == SchemeType::NONE) {
|
|
switch (protocolHint()) {
|
|
case ProtocolHint::RING:
|
|
case ProtocolHint::RING_USERNAME:
|
|
header_type = SchemeType::RING;
|
|
break;
|
|
case ProtocolHint::SIP_HOST:
|
|
case ProtocolHint::SIP_OTHER:
|
|
case ProtocolHint::IP:
|
|
header_type = SchemeType::SIP;
|
|
break;
|
|
case ProtocolHint::UNRECOGNIZED:
|
|
default:
|
|
header_type = SchemeType::RING;
|
|
break;
|
|
}
|
|
}
|
|
|
|
QString ret;
|
|
|
|
if (sections & URI::Section::CHEVRONS)
|
|
ret += '<';
|
|
|
|
if (sections & URI::Section::SCHEME) {
|
|
if (header_type == SchemeType::UNRECOGNIZED) {
|
|
ret += pimpl_->m_Scheme;
|
|
} else {
|
|
ret += URIPimpl::schemeNames.at(header_type);
|
|
}
|
|
}
|
|
|
|
if (sections & URI::Section::USER_INFO)
|
|
ret += pimpl_->m_Userinfo;
|
|
|
|
if (sections & URI::Section::HOSTNAME && !pimpl_->m_Hostname2.isEmpty())
|
|
ret += '@' + pimpl_->m_Hostname2;
|
|
|
|
if (sections & URI::Section::PORT && pimpl_->m_Port != -1)
|
|
ret += ':' + QString::number(pimpl_->m_Port);
|
|
|
|
if (sections & URI::Section::CHEVRONS)
|
|
ret += '>';
|
|
|
|
if (sections & URI::Section::TRANSPORT && pimpl_->m_Transport != URI::Transport::NOT_SET)
|
|
ret += ";transport=" + QString(URIPimpl::transportNames.at(pimpl_->m_Transport));
|
|
|
|
if (sections & URI::Section::TAG && !pimpl_->m_Tag.isEmpty())
|
|
ret += ";tag=" + pimpl_->m_Tag;
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Helper function which returns a QString containing a uri formatted to include at minimum the
|
|
* SCHEME and USER_INFO, and also the HOSTNAME and PORT, if available.
|
|
*/
|
|
QString
|
|
URI::full() const
|
|
{
|
|
return format(URI::Section::SCHEME | URI::Section::USER_INFO | URI::Section::HOSTNAME
|
|
| URI::Section::PORT);
|
|
}
|
|
|
|
QDataStream&
|
|
operator<<(QDataStream& stream, const URI::ProtocolHint& ph)
|
|
{
|
|
switch (ph) {
|
|
case URI::ProtocolHint::SIP_OTHER:
|
|
stream << QStringLiteral("SIP_OTHER");
|
|
break;
|
|
case URI::ProtocolHint::RING:
|
|
case URI::ProtocolHint::RING_USERNAME:
|
|
stream << QStringLiteral("RING");
|
|
break;
|
|
case URI::ProtocolHint::IP:
|
|
stream << QStringLiteral("IP");
|
|
break;
|
|
case URI::ProtocolHint::SIP_HOST:
|
|
stream << QStringLiteral("SIP_HOST");
|
|
break;
|
|
case URI::ProtocolHint::UNRECOGNIZED:
|
|
stream << QStringLiteral("UNRECOGNIZED");
|
|
break;
|
|
}
|
|
|
|
return stream;
|
|
}
|