096de5fe8c9473b318bd3c72b62c69e540b30e29
[WebKit-https.git] / Source / WebCore / platform / network / soup / SoupNetworkSession.cpp
1 /*
2  * Copyright (C) 2014 Igalia S.L.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
14  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
17  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #include "config.h"
27
28 #if USE(SOUP)
29
30 #include "SoupNetworkSession.h"
31
32 #include "AuthenticationChallenge.h"
33 #include "GUniquePtrSoup.h"
34 #include "Logging.h"
35 #include "SoupNetworkProxySettings.h"
36 #include <glib/gstdio.h>
37 #include <libsoup/soup.h>
38 #include <pal/crypto/CryptoDigest.h>
39 #include <wtf/FileSystem.h>
40 #include <wtf/HashSet.h>
41 #include <wtf/NeverDestroyed.h>
42 #include <wtf/text/Base64.h>
43 #include <wtf/text/CString.h>
44
45 namespace WebCore {
46
47 static bool gIgnoreTLSErrors;
48 static GType gCustomProtocolRequestType;
49
50 static CString& initialAcceptLanguages()
51 {
52     static NeverDestroyed<CString> storage;
53     return storage.get();
54 }
55
56 static SoupNetworkProxySettings& proxySettings()
57 {
58     static NeverDestroyed<SoupNetworkProxySettings> settings;
59     return settings.get();
60 }
61
62 static CString& hstsStorageDirectory()
63 {
64     static NeverDestroyed<CString> directory;
65     return directory.get();
66 }
67
68 #if !LOG_DISABLED
69 inline static void soupLogPrinter(SoupLogger*, SoupLoggerLogLevel, char direction, const char* data, gpointer)
70 {
71     LOG(Network, "%c %s", direction, data);
72 }
73 #endif
74
75 class HostTLSCertificateSet {
76 public:
77     void add(GTlsCertificate* certificate)
78     {
79         String certificateHash = computeCertificateHash(certificate);
80         if (!certificateHash.isEmpty())
81             m_certificates.add(certificateHash);
82     }
83
84     bool contains(GTlsCertificate* certificate) const
85     {
86         return m_certificates.contains(computeCertificateHash(certificate));
87     }
88
89 private:
90     static String computeCertificateHash(GTlsCertificate* certificate)
91     {
92         GRefPtr<GByteArray> certificateData;
93         g_object_get(G_OBJECT(certificate), "certificate", &certificateData.outPtr(), nullptr);
94         if (!certificateData)
95             return String();
96
97         auto digest = PAL::CryptoDigest::create(PAL::CryptoDigest::Algorithm::SHA_256);
98         digest->addBytes(certificateData->data, certificateData->len);
99
100         auto hash = digest->computeHash();
101         return base64Encode(reinterpret_cast<const char*>(hash.data()), hash.size());
102     }
103
104     HashSet<String> m_certificates;
105 };
106
107 using AllowedCertificatesMap = HashMap<String, HostTLSCertificateSet, ASCIICaseInsensitiveHash>;
108
109 static AllowedCertificatesMap& allowedCertificates()
110 {
111     static NeverDestroyed<AllowedCertificatesMap> certificates;
112     return certificates;
113 }
114
115 SoupNetworkSession::SoupNetworkSession(PAL::SessionID sessionID)
116     : m_soupSession(adoptGRef(soup_session_new()))
117     , m_sessionID(sessionID)
118 {
119     // Values taken from http://www.browserscope.org/ following
120     // the rule "Do What Every Other Modern Browser Is Doing". They seem
121     // to significantly improve page loading time compared to soup's
122     // default values.
123     static const int maxConnections = 17;
124     static const int maxConnectionsPerHost = 6;
125
126     g_object_set(m_soupSession.get(),
127         SOUP_SESSION_MAX_CONNS, maxConnections,
128         SOUP_SESSION_MAX_CONNS_PER_HOST, maxConnectionsPerHost,
129         SOUP_SESSION_TIMEOUT, 0,
130         SOUP_SESSION_IDLE_TIMEOUT, 0,
131         SOUP_SESSION_ADD_FEATURE_BY_TYPE, SOUP_TYPE_CONTENT_SNIFFER,
132 #if SOUP_CHECK_VERSION(2, 67, 90)
133         SOUP_SESSION_ADD_FEATURE_BY_TYPE, SOUP_TYPE_WEBSOCKET_EXTENSION_MANAGER,
134 #endif
135         nullptr);
136
137     setupCustomProtocols();
138
139     if (!initialAcceptLanguages().isNull())
140         setAcceptLanguages(initialAcceptLanguages());
141
142     if (soup_auth_negotiate_supported() && !m_sessionID.isEphemeral()) {
143         g_object_set(m_soupSession.get(),
144             SOUP_SESSION_ADD_FEATURE_BY_TYPE, SOUP_TYPE_AUTH_NEGOTIATE,
145             nullptr);
146     }
147
148     if (proxySettings().mode != SoupNetworkProxySettings::Mode::Default)
149         setupProxy();
150     setupLogger();
151     setupHSTSEnforcer();
152 }
153
154 SoupNetworkSession::~SoupNetworkSession() = default;
155
156 void SoupNetworkSession::setupLogger()
157 {
158 #if !LOG_DISABLED
159     if (LogNetwork.state != WTFLogChannelState::On || soup_session_get_feature(m_soupSession.get(), SOUP_TYPE_LOGGER))
160         return;
161
162     GRefPtr<SoupLogger> logger = adoptGRef(soup_logger_new(SOUP_LOGGER_LOG_BODY, -1));
163     soup_session_add_feature(m_soupSession.get(), SOUP_SESSION_FEATURE(logger.get()));
164     soup_logger_set_printer(logger.get(), soupLogPrinter, nullptr, nullptr);
165 #endif
166 }
167
168 void SoupNetworkSession::setCookieJar(SoupCookieJar* jar)
169 {
170     if (SoupCookieJar* currentJar = cookieJar())
171         soup_session_remove_feature(m_soupSession.get(), SOUP_SESSION_FEATURE(currentJar));
172     soup_session_add_feature(m_soupSession.get(), SOUP_SESSION_FEATURE(jar));
173 }
174
175 SoupCookieJar* SoupNetworkSession::cookieJar() const
176 {
177     return SOUP_COOKIE_JAR(soup_session_get_feature(m_soupSession.get(), SOUP_TYPE_COOKIE_JAR));
178 }
179
180 void SoupNetworkSession::setHSTSPersistentStorage(const CString& directory)
181 {
182     hstsStorageDirectory() = directory;
183 }
184
185 void SoupNetworkSession::setupHSTSEnforcer()
186 {
187 #if SOUP_CHECK_VERSION(2, 67, 1)
188     if (soup_session_has_feature(m_soupSession.get(), SOUP_TYPE_HSTS_ENFORCER))
189         soup_session_remove_feature_by_type(m_soupSession.get(), SOUP_TYPE_HSTS_ENFORCER);
190
191     GRefPtr<SoupHSTSEnforcer> enforcer;
192     if (m_sessionID.isEphemeral() || hstsStorageDirectory().isNull())
193         enforcer = adoptGRef(soup_hsts_enforcer_new());
194     else {
195         if (FileSystem::makeAllDirectories(hstsStorageDirectory().data())) {
196             CString storagePath = FileSystem::fileSystemRepresentation(hstsStorageDirectory().data());
197             GUniquePtr<char> dbFilename(g_build_filename(storagePath.data(), "hsts-storage.sqlite", nullptr));
198             enforcer = adoptGRef(soup_hsts_enforcer_db_new(dbFilename.get()));
199         } else {
200             RELEASE_LOG_ERROR(Network, "Unable to create the HSTS storage directory \"%s\". Using a memory enforcer instead.", hstsStorageDirectory().data());
201             enforcer = adoptGRef(soup_hsts_enforcer_new());
202         }
203     }
204     soup_session_add_feature(m_soupSession.get(), SOUP_SESSION_FEATURE(enforcer.get()));
205 #endif
206 }
207
208 void SoupNetworkSession::getHostNamesWithHSTSCache(HashSet<String>& hostNames)
209 {
210 #if SOUP_CHECK_VERSION(2, 67, 91)
211     SoupHSTSEnforcer* enforcer = SOUP_HSTS_ENFORCER(soup_session_get_feature(m_soupSession.get(), SOUP_TYPE_HSTS_ENFORCER));
212     if (!enforcer)
213         return;
214
215     GUniquePtr<GList> domains(soup_hsts_enforcer_get_domains(enforcer, FALSE));
216     for (GList* iter = domains.get(); iter; iter = iter->next) {
217         GUniquePtr<gchar> domain(static_cast<gchar*>(iter->data));
218         hostNames.add(String::fromUTF8(domain.get()));
219     }
220 #else
221     UNUSED_PARAM(hostNames);
222 #endif
223 }
224
225 void SoupNetworkSession::deleteHSTSCacheForHostNames(const Vector<String>& hostNames)
226 {
227 #if SOUP_CHECK_VERSION(2, 67, 1)
228     SoupHSTSEnforcer* enforcer = SOUP_HSTS_ENFORCER(soup_session_get_feature(m_soupSession.get(), SOUP_TYPE_HSTS_ENFORCER));
229     if (!enforcer)
230         return;
231
232     for (const auto& hostName : hostNames) {
233         GUniquePtr<SoupHSTSPolicy> policy(soup_hsts_policy_new(hostName.utf8().data(), SOUP_HSTS_POLICY_MAX_AGE_PAST, FALSE));
234         soup_hsts_enforcer_set_policy(enforcer, policy.get());
235     }
236 #else
237     UNUSED_PARAM(hostNames);
238 #endif
239 }
240
241 void SoupNetworkSession::clearHSTSCache(WallTime modifiedSince)
242 {
243 #if SOUP_CHECK_VERSION(2, 67, 91)
244     SoupHSTSEnforcer* enforcer = SOUP_HSTS_ENFORCER(soup_session_get_feature(m_soupSession.get(), SOUP_TYPE_HSTS_ENFORCER));
245     if (!enforcer)
246         return;
247
248     GUniquePtr<GList> policies(soup_hsts_enforcer_get_policies(enforcer, FALSE));
249     for (GList* iter = policies.get(); iter != nullptr; iter = iter->next) {
250         GUniquePtr<SoupHSTSPolicy> policy(static_cast<SoupHSTSPolicy*>(iter->data));
251         auto modified = soup_date_to_time_t(policy.get()->expires) - policy.get()->max_age;
252         if (modified >= modifiedSince.secondsSinceEpoch().seconds()) {
253             GUniquePtr<SoupHSTSPolicy> newPolicy(soup_hsts_policy_new(policy.get()->domain, SOUP_HSTS_POLICY_MAX_AGE_PAST, FALSE));
254             soup_hsts_enforcer_set_policy(enforcer, newPolicy.get());
255         }
256     }
257 #else
258     UNUSED_PARAM(modifiedSince);
259 #endif
260 }
261
262 static inline bool stringIsNumeric(const char* str)
263 {
264     while (*str) {
265         if (!g_ascii_isdigit(*str))
266             return false;
267         str++;
268     }
269     return true;
270 }
271
272 // Old versions of WebKit created this cache.
273 void SoupNetworkSession::clearOldSoupCache(const String& cacheDirectory)
274 {
275     CString cachePath = FileSystem::fileSystemRepresentation(cacheDirectory);
276     GUniquePtr<char> cacheFile(g_build_filename(cachePath.data(), "soup.cache2", nullptr));
277     if (!g_file_test(cacheFile.get(), G_FILE_TEST_IS_REGULAR))
278         return;
279
280     GUniquePtr<GDir> dir(g_dir_open(cachePath.data(), 0, nullptr));
281     if (!dir)
282         return;
283
284     while (const char* name = g_dir_read_name(dir.get())) {
285         if (!g_str_has_prefix(name, "soup.cache") && !stringIsNumeric(name))
286             continue;
287
288         GUniquePtr<gchar> filename(g_build_filename(cachePath.data(), name, nullptr));
289         if (g_file_test(filename.get(), G_FILE_TEST_IS_REGULAR))
290             g_unlink(filename.get());
291     }
292 }
293
294 void SoupNetworkSession::setupProxy()
295 {
296     GRefPtr<GProxyResolver> resolver;
297     switch (proxySettings().mode) {
298     case SoupNetworkProxySettings::Mode::Default: {
299         GRefPtr<GProxyResolver> currentResolver;
300         g_object_get(m_soupSession.get(), SOUP_SESSION_PROXY_RESOLVER, &currentResolver.outPtr(), nullptr);
301         GProxyResolver* defaultResolver = g_proxy_resolver_get_default();
302         if (currentResolver.get() == defaultResolver)
303             return;
304         resolver = defaultResolver;
305         break;
306     }
307     case SoupNetworkProxySettings::Mode::NoProxy:
308         // Do nothing in this case, resolver is nullptr so that when set it will disable proxies.
309         break;
310     case SoupNetworkProxySettings::Mode::Custom:
311         resolver = adoptGRef(g_simple_proxy_resolver_new(nullptr, nullptr));
312         if (!proxySettings().defaultProxyURL.isNull())
313             g_simple_proxy_resolver_set_default_proxy(G_SIMPLE_PROXY_RESOLVER(resolver.get()), proxySettings().defaultProxyURL.data());
314         if (proxySettings().ignoreHosts)
315             g_simple_proxy_resolver_set_ignore_hosts(G_SIMPLE_PROXY_RESOLVER(resolver.get()), proxySettings().ignoreHosts.get());
316         for (const auto& iter : proxySettings().proxyMap)
317             g_simple_proxy_resolver_set_uri_proxy(G_SIMPLE_PROXY_RESOLVER(resolver.get()), iter.key.data(), iter.value.data());
318         break;
319     }
320
321     g_object_set(m_soupSession.get(), SOUP_SESSION_PROXY_RESOLVER, resolver.get(), nullptr);
322     soup_session_abort(m_soupSession.get());
323 }
324
325 void SoupNetworkSession::setProxySettings(const SoupNetworkProxySettings& settings)
326 {
327     proxySettings() = settings;
328 }
329
330 void SoupNetworkSession::setInitialAcceptLanguages(const CString& languages)
331 {
332     initialAcceptLanguages() = languages;
333 }
334
335 void SoupNetworkSession::setAcceptLanguages(const CString& languages)
336 {
337     g_object_set(m_soupSession.get(), "accept-language", languages.data(), nullptr);
338 }
339
340 void SoupNetworkSession::setCustomProtocolRequestType(GType requestType)
341 {
342     ASSERT(g_type_is_a(requestType, SOUP_TYPE_REQUEST));
343     gCustomProtocolRequestType = requestType;
344 }
345
346 void SoupNetworkSession::setupCustomProtocols()
347 {
348     if (!g_type_is_a(gCustomProtocolRequestType, SOUP_TYPE_REQUEST))
349         return;
350
351     auto* requestClass = static_cast<SoupRequestClass*>(g_type_class_peek(gCustomProtocolRequestType));
352     if (!requestClass || !requestClass->schemes)
353         return;
354
355     soup_session_add_feature_by_type(m_soupSession.get(), gCustomProtocolRequestType);
356 }
357
358 void SoupNetworkSession::setShouldIgnoreTLSErrors(bool ignoreTLSErrors)
359 {
360     gIgnoreTLSErrors = ignoreTLSErrors;
361 }
362
363 Optional<ResourceError> SoupNetworkSession::checkTLSErrors(const URL& requestURL, GTlsCertificate* certificate, GTlsCertificateFlags tlsErrors)
364 {
365     if (gIgnoreTLSErrors)
366         return WTF::nullopt;
367
368     if (!tlsErrors)
369         return WTF::nullopt;
370
371     auto it = allowedCertificates().find(requestURL.host().toStringWithoutCopying());
372     if (it != allowedCertificates().end() && it->value.contains(certificate))
373         return WTF::nullopt;
374
375     return ResourceError::tlsError(requestURL, tlsErrors, certificate);
376 }
377
378 void SoupNetworkSession::allowSpecificHTTPSCertificateForHost(const CertificateInfo& certificateInfo, const String& host)
379 {
380     allowedCertificates().add(host, HostTLSCertificateSet()).iterator->value.add(certificateInfo.certificate());
381 }
382
383 } // namespace WebCore
384
385 #endif