Revision control
Copy as Markdown
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant reason:trusted-data
#include "qnetworkproxy.h"
#ifndef QT_NO_NETWORKPROXY
#include <CFNetwork/CFNetwork.h>
#include <CoreFoundation/CoreFoundation.h>
#include <SystemConfiguration/SystemConfiguration.h>
#include <QtCore/QRegularExpression>
#include <QtCore/QStringList>
#include <QtCore/QUrl>
#include <QtCore/qendian.h>
#include <QtCore/qstringlist.h>
#include <QtCore/qsystemdetection.h>
#include "private/qcore_mac_p.h"
/*
* MacOS X has a proxy configuration module in System Preferences (on
* MacOS X 10.5, it's in Network, Advanced), where one can set the
* proxy settings for:
*
* \list
* \li FTP proxy
* \li Web Proxy (HTTP)
* \li Secure Web Proxy (HTTPS)
* \li Streaming Proxy (RTSP)
* \li SOCKS Proxy
* \li Gopher Proxy
* \li URL for Automatic Proxy Configuration (PAC scripts)
* \li Bypass list (by default: *.local, 169.254/16)
* \endlist
*
* The matching configuration can be obtained by calling CFNetworkCopySystemProxySettings()
* (from <CFNetwork/CFProxySupport.h>). See
* Apple's documentation:
*
*
*/
QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
static bool isHostExcluded(CFDictionaryRef dict, const QString &host)
{
Q_ASSERT(dict);
if (host.isEmpty())
return true;
#ifndef Q_OS_IOS
// On iOS all those keys are not available, and worse so - entries
// for HTTPS are not in the dictionary, but instead in some nested dictionary
// with undocumented keys/object types.
bool isSimple = !host.contains(u'.') && !host.contains(u':');
CFNumberRef excludeSimples;
if (isSimple &&
(excludeSimples = (CFNumberRef)CFDictionaryGetValue(dict, kCFNetworkProxiesExcludeSimpleHostnames))) {
int enabled;
if (CFNumberGetValue(excludeSimples, kCFNumberIntType, &enabled) && enabled)
return true;
}
QHostAddress ipAddress;
bool isIpAddress = ipAddress.setAddress(host);
// not a simple host name
// does it match the list of exclusions?
CFArrayRef exclusionList = (CFArrayRef)CFDictionaryGetValue(dict, kCFNetworkProxiesExceptionsList);
if (!exclusionList)
return false;
CFIndex size = CFArrayGetCount(exclusionList);
for (CFIndex i = 0; i < size; ++i) {
CFStringRef cfentry = (CFStringRef)CFArrayGetValueAtIndex(exclusionList, i);
QString entry = QString::fromCFString(cfentry);
if (isIpAddress && ipAddress.isInSubnet(QHostAddress::parseSubnet(entry))) {
return true; // excluded
} else {
// do wildcard matching
auto rx = QRegularExpression::fromWildcard(entry, Qt::CaseInsensitive);
if (rx.match(host).hasMatch())
return true;
}
}
#else
Q_UNUSED(dict);
#endif // Q_OS_IOS
// host was not excluded
return false;
}
static QNetworkProxy proxyFromDictionary(CFDictionaryRef dict, QNetworkProxy::ProxyType type,
CFStringRef enableKey, CFStringRef hostKey,
CFStringRef portKey)
{
CFNumberRef protoEnabled;
CFNumberRef protoPort;
CFStringRef protoHost;
if (enableKey
&& (protoEnabled = (CFNumberRef)CFDictionaryGetValue(dict, enableKey))
&& (protoHost = (CFStringRef)CFDictionaryGetValue(dict, hostKey))
&& (protoPort = (CFNumberRef)CFDictionaryGetValue(dict, portKey))) {
int enabled;
if (CFNumberGetValue(protoEnabled, kCFNumberIntType, &enabled) && enabled) {
QString host = QString::fromCFString(protoHost);
int port;
CFNumberGetValue(protoPort, kCFNumberIntType, &port);
return QNetworkProxy(type, host, port);
}
}
// proxy not enabled
return QNetworkProxy();
}
static QNetworkProxy proxyFromDictionary(CFDictionaryRef dict)
{
QNetworkProxy::ProxyType proxyType = QNetworkProxy::DefaultProxy;
QString hostName;
quint16 port = 0;
QString user;
QString password;
CFStringRef cfProxyType = (CFStringRef)CFDictionaryGetValue(dict, kCFProxyTypeKey);
if (CFStringCompare(cfProxyType, kCFProxyTypeNone, 0) == kCFCompareEqualTo) {
proxyType = QNetworkProxy::NoProxy;
} else if (CFStringCompare(cfProxyType, kCFProxyTypeFTP, 0) == kCFCompareEqualTo) {
proxyType = QNetworkProxy::FtpCachingProxy;
} else if (CFStringCompare(cfProxyType, kCFProxyTypeHTTP, 0) == kCFCompareEqualTo) {
proxyType = QNetworkProxy::HttpProxy;
} else if (CFStringCompare(cfProxyType, kCFProxyTypeHTTPS, 0) == kCFCompareEqualTo) {
proxyType = QNetworkProxy::HttpProxy;
} else if (CFStringCompare(cfProxyType, kCFProxyTypeSOCKS, 0) == kCFCompareEqualTo) {
proxyType = QNetworkProxy::Socks5Proxy;
}
hostName = QString::fromCFString((CFStringRef)CFDictionaryGetValue(dict, kCFProxyHostNameKey));
user = QString::fromCFString((CFStringRef)CFDictionaryGetValue(dict, kCFProxyUsernameKey));
password = QString::fromCFString((CFStringRef)CFDictionaryGetValue(dict, kCFProxyPasswordKey));
CFNumberRef portNumber = (CFNumberRef)CFDictionaryGetValue(dict, kCFProxyPortNumberKey);
if (portNumber) {
CFNumberGetValue(portNumber, kCFNumberSInt16Type, &port);
}
return QNetworkProxy(proxyType, hostName, port, user, password);
}
namespace {
struct PACInfo {
QCFType<CFArrayRef> proxies;
QCFType<CFErrorRef> error;
bool done = false;
};
void proxyAutoConfigCallback(void *client, CFArrayRef proxylist, CFErrorRef error)
{
Q_ASSERT(client);
PACInfo *info = static_cast<PACInfo *>(client);
info->done = true;
if (error) {
CFRetain(error);
info->error = error;
}
if (proxylist) {
CFRetain(proxylist);
info->proxies = proxylist;
}
}
QCFType<CFStringRef> stringByAddingPercentEscapes(CFStringRef originalPath)
{
Q_ASSERT(originalPath);
const auto qtPath = QString::fromCFString(originalPath);
const auto escaped = QString::fromUtf8(QUrl(qtPath).toEncoded());
return escaped.toCFString();
}
#ifdef Q_OS_IOS
QList<QNetworkProxy> proxiesForQueryUrl(CFDictionaryRef dict, const QUrl &url)
{
Q_ASSERT(dict);
const QCFType<CFURLRef> cfUrl = url.toCFURL();
const QCFType<CFArrayRef> proxies = CFNetworkCopyProxiesForURL(cfUrl, dict);
Q_ASSERT(proxies);
QList<QNetworkProxy> result;
const auto count = CFArrayGetCount(proxies);
if (!count) // Could be no proper proxy or host excluded.
return result;
for (CFIndex i = 0; i < count; ++i) {
const void *obj = CFArrayGetValueAtIndex(proxies, i);
if (CFGetTypeID(obj) != CFDictionaryGetTypeID())
continue;
const QNetworkProxy proxy = proxyFromDictionary(static_cast<CFDictionaryRef>(obj));
if (proxy.type() == QNetworkProxy::NoProxy || proxy.type() == QNetworkProxy::DefaultProxy)
continue;
result << proxy;
}
return result;
}
#endif // Q_OS_IOS
} // unnamed namespace.
QList<QNetworkProxy> macQueryInternal(const QNetworkProxyQuery &query)
{
QList<QNetworkProxy> result;
// obtain a dictionary to the proxy settings:
const QCFType<CFDictionaryRef> dict = CFNetworkCopySystemProxySettings();
if (!dict) {
qWarning("QNetworkProxyFactory::systemProxyForQuery: CFNetworkCopySystemProxySettings returned nullptr");
return result; // failed
}
if (isHostExcluded(dict, query.peerHostName()))
return result; // no proxy for this host
// is there a PAC enabled? If so, use it first.
CFNumberRef pacEnabled;
if ((pacEnabled = (CFNumberRef)CFDictionaryGetValue(dict, kCFNetworkProxiesProxyAutoConfigEnable))) {
int enabled;
if (CFNumberGetValue(pacEnabled, kCFNumberIntType, &enabled) && enabled) {
// PAC is enabled
// kSCPropNetProxiesProxyAutoConfigURLString returns the URL string
// as entered in the system proxy configuration dialog
CFStringRef pacLocationSetting = (CFStringRef)CFDictionaryGetValue(dict, kCFNetworkProxiesProxyAutoConfigURLString);
auto cfPacLocation = stringByAddingPercentEscapes(pacLocationSetting);
QCFType<CFDataRef> pacData;
QCFType<CFURLRef> pacUrl = CFURLCreateWithString(kCFAllocatorDefault, cfPacLocation, NULL);
if (!pacUrl) {
qWarning("Invalid PAC URL \"%s\"", qPrintable(QString::fromCFString(cfPacLocation)));
return result;
}
QByteArray encodedURL = query.url().toEncoded(); // converted to UTF-8
if (encodedURL.isEmpty()) {
return result; // Invalid URL, abort
}
QCFType<CFURLRef> targetURL = CFURLCreateWithBytes(kCFAllocatorDefault, (UInt8*)encodedURL.data(), encodedURL.size(), kCFStringEncodingUTF8, NULL);
if (!targetURL) {
return result; // URL creation problem, abort
}
CFStreamClientContext pacCtx;
pacCtx.version = 0;
PACInfo pacInfo;
pacCtx.info = &pacInfo;
pacCtx.retain = NULL;
pacCtx.release = NULL;
pacCtx.copyDescription = NULL;
static CFStringRef pacRunLoopMode = CFSTR("qtPACRunLoopMode");
QCFType<CFRunLoopSourceRef> pacRunLoopSource = CFNetworkExecuteProxyAutoConfigurationURL(pacUrl, targetURL, &proxyAutoConfigCallback, &pacCtx);
CFRunLoopAddSource(CFRunLoopGetCurrent(), pacRunLoopSource, pacRunLoopMode);
while (!pacInfo.done)
CFRunLoopRunInMode(pacRunLoopMode, 1000, /*returnAfterSourceHandled*/ true);
if (!pacInfo.proxies) {
QString pacLocation = QString::fromCFString(cfPacLocation);
QCFType<CFStringRef> pacErrorDescription = CFErrorCopyDescription(pacInfo.error);
qWarning("Execution of PAC script at \"%s\" failed: %s", qPrintable(pacLocation), qPrintable(QString::fromCFString(pacErrorDescription)));
return result;
}
CFIndex size = CFArrayGetCount(pacInfo.proxies);
for (CFIndex i = 0; i < size; ++i) {
CFDictionaryRef proxy = (CFDictionaryRef)CFArrayGetValueAtIndex(pacInfo.proxies, i);
result << proxyFromDictionary(proxy);
}
return result;
}
}
// No PAC, decide which proxy we're looking for based on the query
// try the protocol-specific proxy
const QString protocol = query.protocolTag();
QNetworkProxy protocolSpecificProxy;
if (protocol.compare("http"_L1, Qt::CaseInsensitive) == 0) {
protocolSpecificProxy =
proxyFromDictionary(dict, QNetworkProxy::HttpProxy,
kCFNetworkProxiesHTTPEnable,
kCFNetworkProxiesHTTPProxy,
kCFNetworkProxiesHTTPPort);
}
#ifdef Q_OS_IOS
if (protocolSpecificProxy.type() != QNetworkProxy::DefaultProxy
&& protocolSpecificProxy.type() != QNetworkProxy::DefaultProxy) {
// HTTP proxy is enabled (on iOS there is no separate HTTPS, though
// 'dict' contains deeply buried entries which are the same as HTTP.
result << protocolSpecificProxy;
}
// TODO: check query.queryType()? It's possible, the exclude list
// did exclude it but above we added a proxy because HTTP proxy
// is found. We'll deal with such a situation later, since now NMI.
const auto proxiesForUrl = proxiesForQueryUrl(dict, query.url());
for (const auto &proxy : proxiesForUrl) {
if (!result.contains(proxy))
result << proxy;
}
#else
bool isHttps = false;
if (protocol.compare("ftp"_L1, Qt::CaseInsensitive) == 0) {
protocolSpecificProxy =
proxyFromDictionary(dict, QNetworkProxy::FtpCachingProxy,
kCFNetworkProxiesFTPEnable,
kCFNetworkProxiesFTPProxy,
kCFNetworkProxiesFTPPort);
} else if (protocol.compare("https"_L1, Qt::CaseInsensitive) == 0) {
isHttps = true;
protocolSpecificProxy =
proxyFromDictionary(dict, QNetworkProxy::HttpProxy,
kCFNetworkProxiesHTTPSEnable,
kCFNetworkProxiesHTTPSProxy,
kCFNetworkProxiesHTTPSPort);
}
if (protocolSpecificProxy.type() != QNetworkProxy::DefaultProxy)
result << protocolSpecificProxy;
// let's add SOCKSv5 if present too
QNetworkProxy socks5 = proxyFromDictionary(dict, QNetworkProxy::Socks5Proxy,
kCFNetworkProxiesSOCKSEnable,
kCFNetworkProxiesSOCKSProxy,
kCFNetworkProxiesSOCKSPort);
if (socks5.type() != QNetworkProxy::DefaultProxy)
result << socks5;
// let's add the HTTPS proxy if present (and if we haven't added
// yet)
if (!isHttps) {
QNetworkProxy https;
https = proxyFromDictionary(dict, QNetworkProxy::HttpProxy,
kCFNetworkProxiesHTTPSEnable,
kCFNetworkProxiesHTTPSProxy,
kCFNetworkProxiesHTTPSPort);
if (https.type() != QNetworkProxy::DefaultProxy && https != protocolSpecificProxy)
result << https;
}
#endif // !Q_OS_IOS
return result;
}
QList<QNetworkProxy> QNetworkProxyFactory::systemProxyForQuery(const QNetworkProxyQuery &query)
{
QList<QNetworkProxy> result = macQueryInternal(query);
if (result.isEmpty())
result << QNetworkProxy::NoProxy;
return result;
}
QT_END_NAMESPACE
#endif // QT_NO_NETWORKPROXY