Introducing the Platform Abstraction Layer (PAL)
[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 "FileSystem.h"
34 #include "GUniquePtrSoup.h"
35 #include "Logging.h"
36 #include "ResourceHandle.h"
37 #include "SoupNetworkProxySettings.h"
38 #include <glib/gstdio.h>
39 #include <libsoup/soup.h>
40 #include <pal/crypto/CryptoDigest.h>
41 #include <wtf/HashSet.h>
42 #include <wtf/NeverDestroyed.h>
43 #include <wtf/text/Base64.h>
44 #include <wtf/text/CString.h>
45
46 namespace WebCore {
47
48 static bool gIgnoreTLSErrors;
49 static CString gInitialAcceptLanguages;
50 static SoupNetworkProxySettings gProxySettings;
51
52 #if !LOG_DISABLED
53 inline static void soupLogPrinter(SoupLogger*, SoupLoggerLogLevel, char direction, const char* data, gpointer)
54 {
55     LOG(Network, "%c %s", direction, data);
56 }
57 #endif
58
59 class HostTLSCertificateSet {
60 public:
61     void add(GTlsCertificate* certificate)
62     {
63         String certificateHash = computeCertificateHash(certificate);
64         if (!certificateHash.isEmpty())
65             m_certificates.add(certificateHash);
66     }
67
68     bool contains(GTlsCertificate* certificate) const
69     {
70         return m_certificates.contains(computeCertificateHash(certificate));
71     }
72
73 private:
74     static String computeCertificateHash(GTlsCertificate* certificate)
75     {
76         GRefPtr<GByteArray> certificateData;
77         g_object_get(G_OBJECT(certificate), "certificate", &certificateData.outPtr(), nullptr);
78         if (!certificateData)
79             return String();
80
81         auto digest = PAL::CryptoDigest::create(PAL::CryptoDigest::Algorithm::SHA_256);
82         digest->addBytes(certificateData->data, certificateData->len);
83
84         auto hash = digest->computeHash();
85         return base64Encode(reinterpret_cast<const char*>(hash.data()), hash.size());
86     }
87
88     HashSet<String> m_certificates;
89 };
90
91 static HashMap<String, HostTLSCertificateSet, ASCIICaseInsensitiveHash>& clientCertificates()
92 {
93     static NeverDestroyed<HashMap<String, HostTLSCertificateSet, ASCIICaseInsensitiveHash>> certificates;
94     return certificates;
95 }
96
97 static void authenticateCallback(SoupSession*, SoupMessage* soupMessage, SoupAuth* soupAuth, gboolean retrying)
98 {
99     RefPtr<ResourceHandle> handle = static_cast<ResourceHandle*>(g_object_get_data(G_OBJECT(soupMessage), "handle"));
100     if (!handle)
101         return;
102     handle->didReceiveAuthenticationChallenge(AuthenticationChallenge(soupMessage, soupAuth, retrying, handle.get()));
103 }
104
105 #if ENABLE(WEB_TIMING) && !SOUP_CHECK_VERSION(2, 49, 91)
106 static void requestStartedCallback(SoupSession*, SoupMessage* soupMessage, SoupSocket*, gpointer)
107 {
108     RefPtr<ResourceHandle> handle = static_cast<ResourceHandle*>(g_object_get_data(G_OBJECT(soupMessage), "handle"));
109     if (!handle)
110         return;
111     handle->didStartRequest();
112 }
113 #endif
114
115 SoupNetworkSession::SoupNetworkSession(SoupCookieJar* cookieJar)
116     : m_soupSession(adoptGRef(soup_session_async_new()))
117 {
118     // Values taken from http://www.browserscope.org/ following
119     // the rule "Do What Every Other Modern Browser Is Doing". They seem
120     // to significantly improve page loading time compared to soup's
121     // default values.
122     static const int maxConnections = 17;
123     static const int maxConnectionsPerHost = 6;
124
125     GRefPtr<SoupCookieJar> jar = cookieJar;
126     if (!jar) {
127         jar = adoptGRef(soup_cookie_jar_new());
128         soup_cookie_jar_set_accept_policy(jar.get(), SOUP_COOKIE_JAR_ACCEPT_NO_THIRD_PARTY);
129     }
130
131     g_object_set(m_soupSession.get(),
132         SOUP_SESSION_MAX_CONNS, maxConnections,
133         SOUP_SESSION_MAX_CONNS_PER_HOST, maxConnectionsPerHost,
134         SOUP_SESSION_ADD_FEATURE_BY_TYPE, SOUP_TYPE_CONTENT_DECODER,
135         SOUP_SESSION_ADD_FEATURE_BY_TYPE, SOUP_TYPE_CONTENT_SNIFFER,
136         SOUP_SESSION_ADD_FEATURE_BY_TYPE, SOUP_TYPE_PROXY_RESOLVER_DEFAULT,
137         SOUP_SESSION_ADD_FEATURE, jar.get(),
138         SOUP_SESSION_USE_THREAD_CONTEXT, TRUE,
139         SOUP_SESSION_SSL_USE_SYSTEM_CA_FILE, TRUE,
140         SOUP_SESSION_SSL_STRICT, FALSE,
141         nullptr);
142
143     if (!gInitialAcceptLanguages.isNull())
144         setAcceptLanguages(gInitialAcceptLanguages);
145
146 #if SOUP_CHECK_VERSION(2, 53, 92)
147     if (soup_auth_negotiate_supported()) {
148         g_object_set(m_soupSession.get(),
149             SOUP_SESSION_ADD_FEATURE_BY_TYPE, SOUP_TYPE_AUTH_NEGOTIATE,
150             nullptr);
151     }
152 #endif
153
154     if (gProxySettings.mode != SoupNetworkProxySettings::Mode::Default)
155         setupProxy();
156     setupLogger();
157
158     g_signal_connect(m_soupSession.get(), "authenticate", G_CALLBACK(authenticateCallback), nullptr);
159 #if ENABLE(WEB_TIMING) && !SOUP_CHECK_VERSION(2, 49, 91)
160     g_signal_connect(m_soupSession.get(), "request-started", G_CALLBACK(requestStartedCallback), nullptr);
161 #endif
162 }
163
164 SoupNetworkSession::~SoupNetworkSession()
165 {
166 }
167
168 void SoupNetworkSession::setupLogger()
169 {
170 #if !LOG_DISABLED
171     if (LogNetwork.state != WTFLogChannelOn || soup_session_get_feature(m_soupSession.get(), SOUP_TYPE_LOGGER))
172         return;
173
174     GRefPtr<SoupLogger> logger = adoptGRef(soup_logger_new(SOUP_LOGGER_LOG_BODY, -1));
175     soup_session_add_feature(m_soupSession.get(), SOUP_SESSION_FEATURE(logger.get()));
176     soup_logger_set_printer(logger.get(), soupLogPrinter, nullptr, nullptr);
177 #endif
178 }
179
180 void SoupNetworkSession::setCookieJar(SoupCookieJar* jar)
181 {
182     if (SoupCookieJar* currentJar = cookieJar())
183         soup_session_remove_feature(m_soupSession.get(), SOUP_SESSION_FEATURE(currentJar));
184     soup_session_add_feature(m_soupSession.get(), SOUP_SESSION_FEATURE(jar));
185 }
186
187 SoupCookieJar* SoupNetworkSession::cookieJar() const
188 {
189     return SOUP_COOKIE_JAR(soup_session_get_feature(m_soupSession.get(), SOUP_TYPE_COOKIE_JAR));
190 }
191
192 static inline bool stringIsNumeric(const char* str)
193 {
194     while (*str) {
195         if (!g_ascii_isdigit(*str))
196             return false;
197         str++;
198     }
199     return true;
200 }
201
202 // Old versions of WebKit created this cache.
203 void SoupNetworkSession::clearOldSoupCache(const String& cacheDirectory)
204 {
205     CString cachePath = fileSystemRepresentation(cacheDirectory);
206     GUniquePtr<char> cacheFile(g_build_filename(cachePath.data(), "soup.cache2", nullptr));
207     if (!g_file_test(cacheFile.get(), G_FILE_TEST_IS_REGULAR))
208         return;
209
210     GUniquePtr<GDir> dir(g_dir_open(cachePath.data(), 0, nullptr));
211     if (!dir)
212         return;
213
214     while (const char* name = g_dir_read_name(dir.get())) {
215         if (!g_str_has_prefix(name, "soup.cache") && !stringIsNumeric(name))
216             continue;
217
218         GUniquePtr<gchar> filename(g_build_filename(cachePath.data(), name, nullptr));
219         if (g_file_test(filename.get(), G_FILE_TEST_IS_REGULAR))
220             g_unlink(filename.get());
221     }
222 }
223
224 void SoupNetworkSession::setupProxy()
225 {
226     GRefPtr<GProxyResolver> resolver;
227     switch (gProxySettings.mode) {
228     case SoupNetworkProxySettings::Mode::Default: {
229         GRefPtr<GProxyResolver> currentResolver;
230         g_object_get(m_soupSession.get(), SOUP_SESSION_PROXY_RESOLVER, &currentResolver.outPtr(), nullptr);
231         GProxyResolver* defaultResolver = g_proxy_resolver_get_default();
232         if (currentResolver.get() == defaultResolver)
233             return;
234         resolver = defaultResolver;
235         break;
236     }
237     case SoupNetworkProxySettings::Mode::NoProxy:
238         // Do nothing in this case, resolver is nullptr so that when set it will disable proxies.
239         break;
240     case SoupNetworkProxySettings::Mode::Custom:
241         resolver = adoptGRef(g_simple_proxy_resolver_new(nullptr, nullptr));
242         if (!gProxySettings.defaultProxyURL.isNull())
243             g_simple_proxy_resolver_set_default_proxy(G_SIMPLE_PROXY_RESOLVER(resolver.get()), gProxySettings.defaultProxyURL.data());
244         if (gProxySettings.ignoreHosts)
245             g_simple_proxy_resolver_set_ignore_hosts(G_SIMPLE_PROXY_RESOLVER(resolver.get()), gProxySettings.ignoreHosts.get());
246         for (const auto& iter : gProxySettings.proxyMap)
247             g_simple_proxy_resolver_set_uri_proxy(G_SIMPLE_PROXY_RESOLVER(resolver.get()), iter.key.data(), iter.value.data());
248         break;
249     }
250
251     g_object_set(m_soupSession.get(), SOUP_SESSION_PROXY_RESOLVER, resolver.get(), nullptr);
252     soup_session_abort(m_soupSession.get());
253 }
254
255 #if PLATFORM(EFL)
256 // FIXME: This function should not exist at all and we don't want to accidentally use it in other ports.
257 // The correct way to set proxy settings from the environment is to use a GProxyResolver that does so.
258 // It also lacks the rather important https_proxy and ftp_proxy variables, and the uppercase versions of
259 // all four variables, all of which you almost surely want to be respected if you're setting http_proxy,
260 // and all of which would be supported via the default proxy resolver in non-GNOME/Ubuntu environments
261 // (at least, I think that's right). Additionally, it is incorrect for WebKit to respect this environment
262 // variable when running in a GNOME or Ubuntu environment, where GNOME proxy configuration should be
263 // respected instead. The only reason to retain this function is to not alter the incorrect behavior for EFL.
264 void SoupNetworkSession::setProxySettingsFromEnvironment()
265 {
266     const char* httpProxy = getenv("http_proxy");
267     if (!httpProxy)
268         return;
269
270     gProxySettings.defaultProxyURL = httpProxy;
271     const char* httpProxyExceptions = getenv("no_proxy");
272     if (httpProxyExceptions)
273         gProxySettings.ignoreHosts.reset(g_strsplit(httpProxyExceptions, ",", -1));
274 }
275 #endif
276
277 void SoupNetworkSession::setProxySettings(const SoupNetworkProxySettings& settings)
278 {
279     gProxySettings = settings;
280 }
281
282 void SoupNetworkSession::setInitialAcceptLanguages(const CString& languages)
283 {
284     gInitialAcceptLanguages = languages;
285 }
286
287 void SoupNetworkSession::setAcceptLanguages(const CString& languages)
288 {
289     g_object_set(m_soupSession.get(), "accept-language", languages.data(), nullptr);
290 }
291
292 void SoupNetworkSession::setShouldIgnoreTLSErrors(bool ignoreTLSErrors)
293 {
294     gIgnoreTLSErrors = ignoreTLSErrors;
295 }
296
297 void SoupNetworkSession::checkTLSErrors(SoupRequest* soupRequest, SoupMessage* message, std::function<void (const ResourceError&)>&& completionHandler)
298 {
299     if (gIgnoreTLSErrors) {
300         completionHandler({ });
301         return;
302     }
303
304     GTlsCertificate* certificate = nullptr;
305     GTlsCertificateFlags tlsErrors = static_cast<GTlsCertificateFlags>(0);
306     soup_message_get_https_status(message, &certificate, &tlsErrors);
307     if (!tlsErrors) {
308         completionHandler({ });
309         return;
310     }
311
312     URL url(soup_request_get_uri(soupRequest));
313     auto it = clientCertificates().find(url.host());
314     if (it != clientCertificates().end() && it->value.contains(certificate)) {
315         completionHandler({ });
316         return;
317     }
318
319     completionHandler(ResourceError::tlsError(soupRequest, tlsErrors, certificate));
320 }
321
322 void SoupNetworkSession::allowSpecificHTTPSCertificateForHost(const CertificateInfo& certificateInfo, const String& host)
323 {
324     clientCertificates().add(host, HostTLSCertificateSet()).iterator->value.add(certificateInfo.certificate());
325 }
326
327 } // namespace WebCore
328
329 #endif