1/*
2 * Copyright (C) 2013 Apple Inc. All rights reserved.
3 * Copyright (C) 2013 University of Szeged. All rights reserved.
4 * Copyright (C) 2016 Igalia S.L.
5 * Copyright (C) 2017 Endless Mobile, Inc.
6 *
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions
9 * are met:
10 * 1. Redistributions of source code must retain the above copyright
11 * notice, this list of conditions and the following disclaimer.
12 * 2. Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in the
14 * documentation and/or other materials provided with the distribution.
15 *
16 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS''
17 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
18 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
20 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
26 * THE POSSIBILITY OF SUCH DAMAGE.
27 */
28
29#include "config.h"
30#include "NetworkStorageSession.h"
31
32#if USE(SOUP)
33
34#include "Cookie.h"
35#include "CookieRequestHeaderFieldProxy.h"
36#include "GUniquePtrSoup.h"
37#include "ResourceHandle.h"
38#include "SoupNetworkSession.h"
39#include "URLSoup.h"
40#include <libsoup/soup.h>
41#include <wtf/DateMath.h>
42#include <wtf/MainThread.h>
43#include <wtf/NeverDestroyed.h>
44#include <wtf/glib/GUniquePtr.h>
45
46#if USE(LIBSECRET)
47#include "GRefPtrGtk.h"
48#include <glib/gi18n-lib.h>
49#define SECRET_WITH_UNSTABLE 1
50#define SECRET_API_SUBJECT_TO_CHANGE 1
51#include <libsecret/secret.h>
52#endif
53
54namespace WebCore {
55
56NetworkStorageSession::NetworkStorageSession(PAL::SessionID sessionID, std::unique_ptr<SoupNetworkSession>&& session)
57 : m_sessionID(sessionID)
58 , m_session(WTFMove(session))
59{
60 ASSERT(m_session->cookieJar());
61 g_signal_connect_swapped(m_session->cookieJar(), "changed", G_CALLBACK(cookiesDidChange), this);
62}
63
64NetworkStorageSession::~NetworkStorageSession()
65{
66 clearSoupNetworkSession();
67}
68
69SoupNetworkSession& NetworkStorageSession::soupNetworkSession() const
70{
71 ASSERT(m_session);
72 return *m_session.get();
73};
74
75void NetworkStorageSession::clearSoupNetworkSession()
76{
77 if (m_session) {
78 ASSERT(m_session->cookieJar());
79 g_signal_handlers_disconnect_matched(m_session->cookieJar(), G_SIGNAL_MATCH_DATA, 0, 0, nullptr, nullptr, this);
80 }
81
82 m_session = nullptr;
83 m_cookieObserverHandler = nullptr;
84}
85
86void NetworkStorageSession::cookiesDidChange(NetworkStorageSession* session)
87{
88 if (session->m_cookieObserverHandler)
89 session->m_cookieObserverHandler();
90}
91
92SoupCookieJar* NetworkStorageSession::cookieStorage() const
93{
94 ASSERT(m_session);
95 ASSERT(m_session->cookieJar());
96 return m_session->cookieJar();
97}
98
99void NetworkStorageSession::setCookieStorage(SoupCookieJar* jar)
100{
101 ASSERT(jar);
102 ASSERT(m_session);
103 ASSERT(m_session->cookieJar());
104
105 if (m_session->cookieJar() == jar)
106 return;
107
108 g_signal_handlers_disconnect_matched(m_session->cookieJar(), G_SIGNAL_MATCH_DATA, 0, 0, nullptr, nullptr, this);
109 m_session->setCookieJar(jar);
110 g_signal_connect_swapped(m_session->cookieJar(), "changed", G_CALLBACK(cookiesDidChange), this);
111}
112
113void NetworkStorageSession::setCookieObserverHandler(Function<void ()>&& handler)
114{
115 m_cookieObserverHandler = WTFMove(handler);
116}
117
118#if USE(LIBSECRET)
119static const char* schemeFromProtectionSpaceServerType(ProtectionSpaceServerType serverType)
120{
121 switch (serverType) {
122 case ProtectionSpaceServerHTTP:
123 case ProtectionSpaceProxyHTTP:
124 return SOUP_URI_SCHEME_HTTP;
125 case ProtectionSpaceServerHTTPS:
126 case ProtectionSpaceProxyHTTPS:
127 return SOUP_URI_SCHEME_HTTPS;
128 case ProtectionSpaceServerFTP:
129 case ProtectionSpaceProxyFTP:
130 return SOUP_URI_SCHEME_FTP;
131 case ProtectionSpaceServerFTPS:
132 case ProtectionSpaceProxySOCKS:
133 break;
134 }
135
136 ASSERT_NOT_REACHED();
137 return SOUP_URI_SCHEME_HTTP;
138}
139
140static const char* authTypeFromProtectionSpaceAuthenticationScheme(ProtectionSpaceAuthenticationScheme scheme)
141{
142 switch (scheme) {
143 case ProtectionSpaceAuthenticationSchemeDefault:
144 case ProtectionSpaceAuthenticationSchemeHTTPBasic:
145 return "Basic";
146 case ProtectionSpaceAuthenticationSchemeHTTPDigest:
147 return "Digest";
148 case ProtectionSpaceAuthenticationSchemeNTLM:
149 return "NTLM";
150 case ProtectionSpaceAuthenticationSchemeNegotiate:
151 return "Negotiate";
152 case ProtectionSpaceAuthenticationSchemeHTMLForm:
153 case ProtectionSpaceAuthenticationSchemeClientCertificateRequested:
154 case ProtectionSpaceAuthenticationSchemeServerTrustEvaluationRequested:
155 ASSERT_NOT_REACHED();
156 break;
157 case ProtectionSpaceAuthenticationSchemeOAuth:
158 return "OAuth";
159 case ProtectionSpaceAuthenticationSchemeUnknown:
160 return "unknown";
161 }
162
163 ASSERT_NOT_REACHED();
164 return "unknown";
165}
166
167struct SecretServiceSearchData {
168 SecretServiceSearchData(GCancellable* cancellable, Function<void (Credential&&)>&& completionHandler)
169 : cancellable(cancellable)
170 , completionHandler(WTFMove(completionHandler))
171 {
172 }
173
174 ~SecretServiceSearchData() = default;
175
176 GRefPtr<GCancellable> cancellable;
177 Function<void (Credential&&)> completionHandler;
178};
179#endif // USE(LIBSECRET)
180
181void NetworkStorageSession::getCredentialFromPersistentStorage(const ProtectionSpace& protectionSpace, GCancellable* cancellable, Function<void (Credential&&)>&& completionHandler)
182{
183#if USE(LIBSECRET)
184 if (m_sessionID.isEphemeral()) {
185 completionHandler({ });
186 return;
187 }
188
189 const String& realm = protectionSpace.realm();
190 if (realm.isEmpty()) {
191 completionHandler({ });
192 return;
193 }
194
195 GRefPtr<GHashTable> attributes = adoptGRef(secret_attributes_build(SECRET_SCHEMA_COMPAT_NETWORK,
196 "domain", realm.utf8().data(),
197 "server", protectionSpace.host().utf8().data(),
198 "port", protectionSpace.port(),
199 "protocol", schemeFromProtectionSpaceServerType(protectionSpace.serverType()),
200 "authtype", authTypeFromProtectionSpaceAuthenticationScheme(protectionSpace.authenticationScheme()),
201 nullptr));
202 if (!attributes) {
203 completionHandler({ });
204 return;
205 }
206
207 auto data = std::make_unique<SecretServiceSearchData>(cancellable, WTFMove(completionHandler));
208 secret_service_search(nullptr, SECRET_SCHEMA_COMPAT_NETWORK, attributes.get(),
209 static_cast<SecretSearchFlags>(SECRET_SEARCH_UNLOCK | SECRET_SEARCH_LOAD_SECRETS), cancellable,
210 [](GObject* source, GAsyncResult* result, gpointer userData) {
211 auto data = std::unique_ptr<SecretServiceSearchData>(static_cast<SecretServiceSearchData*>(userData));
212 GUniqueOutPtr<GError> error;
213 GUniquePtr<GList> elements(secret_service_search_finish(SECRET_SERVICE(source), result, &error.outPtr()));
214 if (g_cancellable_is_cancelled(data->cancellable.get()) || error || !elements || !elements->data) {
215 data->completionHandler({ });
216 return;
217 }
218
219 GRefPtr<SecretItem> secretItem = static_cast<SecretItem*>(elements->data);
220 g_list_foreach(elements.get(), reinterpret_cast<GFunc>(reinterpret_cast<GCallback>(g_object_unref)), nullptr);
221 GRefPtr<GHashTable> attributes = adoptGRef(secret_item_get_attributes(secretItem.get()));
222 String user = String::fromUTF8(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "user")));
223 if (user.isEmpty()) {
224 data->completionHandler({ });
225 return;
226 }
227
228 size_t length;
229 GRefPtr<SecretValue> secretValue = adoptGRef(secret_item_get_secret(secretItem.get()));
230 const char* passwordData = secret_value_get(secretValue.get(), &length);
231 data->completionHandler(Credential(user, String::fromUTF8(passwordData, length), CredentialPersistencePermanent));
232 }, data.release());
233#else
234 UNUSED_PARAM(protectionSpace);
235 UNUSED_PARAM(cancellable);
236 completionHandler({ });
237#endif
238}
239
240void NetworkStorageSession::saveCredentialToPersistentStorage(const ProtectionSpace& protectionSpace, const Credential& credential)
241{
242#if USE(LIBSECRET)
243 if (m_sessionID.isEphemeral())
244 return;
245
246 if (credential.isEmpty())
247 return;
248
249 const String& realm = protectionSpace.realm();
250 if (realm.isEmpty())
251 return;
252
253 GRefPtr<GHashTable> attributes = adoptGRef(secret_attributes_build(SECRET_SCHEMA_COMPAT_NETWORK,
254 "domain", realm.utf8().data(),
255 "server", protectionSpace.host().utf8().data(),
256 "port", protectionSpace.port(),
257 "protocol", schemeFromProtectionSpaceServerType(protectionSpace.serverType()),
258 "authtype", authTypeFromProtectionSpaceAuthenticationScheme(protectionSpace.authenticationScheme()),
259 nullptr));
260 if (!attributes)
261 return;
262
263 g_hash_table_insert(attributes.get(), g_strdup("user"), g_strdup(credential.user().utf8().data()));
264 CString utf8Password = credential.password().utf8();
265 GRefPtr<SecretValue> newSecretValue = adoptGRef(secret_value_new(utf8Password.data(), utf8Password.length(), "text/plain"));
266 secret_service_store(nullptr, SECRET_SCHEMA_COMPAT_NETWORK, attributes.get(), SECRET_COLLECTION_DEFAULT, _("WebKitGTK+ password"),
267 newSecretValue.get(), nullptr, nullptr, nullptr);
268#else
269 UNUSED_PARAM(protectionSpace);
270 UNUSED_PARAM(credential);
271#endif
272}
273
274bool NetworkStorageSession::cookiesEnabled() const
275{
276 auto policy = soup_cookie_jar_get_accept_policy(cookieStorage());
277 return policy == SOUP_COOKIE_JAR_ACCEPT_ALWAYS || policy == SOUP_COOKIE_JAR_ACCEPT_NO_THIRD_PARTY;
278}
279
280static inline bool httpOnlyCookieExists(const GSList* cookies, const gchar* name, const gchar* path)
281{
282 for (const GSList* iter = cookies; iter; iter = g_slist_next(iter)) {
283 SoupCookie* cookie = static_cast<SoupCookie*>(iter->data);
284 if (!strcmp(soup_cookie_get_name(cookie), name)
285 && !g_strcmp0(soup_cookie_get_path(cookie), path)) {
286 if (soup_cookie_get_http_only(cookie))
287 return true;
288 break;
289 }
290 }
291 return false;
292}
293
294void NetworkStorageSession::setCookiesFromDOM(const URL& firstParty, const SameSiteInfo&, const URL& url, Optional<uint64_t> frameID, Optional<uint64_t> pageID, const String& value) const
295{
296 UNUSED_PARAM(frameID);
297 UNUSED_PARAM(pageID);
298 GUniquePtr<SoupURI> origin = urlToSoupURI(url);
299 if (!origin)
300 return;
301
302 GUniquePtr<SoupURI> firstPartyURI = urlToSoupURI(firstParty);
303 if (!firstPartyURI)
304 return;
305
306 // Get existing cookies for this origin.
307 SoupCookieJar* jar = cookieStorage();
308 GSList* existingCookies = soup_cookie_jar_get_cookie_list(jar, origin.get(), TRUE);
309
310 for (auto& cookieString : value.split('\n')) {
311 GUniquePtr<SoupCookie> cookie(soup_cookie_parse(cookieString.utf8().data(), origin.get()));
312
313 if (!cookie)
314 continue;
315
316 // Make sure the cookie is not httpOnly since such cookies should not be set from JavaScript.
317 if (soup_cookie_get_http_only(cookie.get()))
318 continue;
319
320 // Make sure we do not overwrite httpOnly cookies from JavaScript.
321 if (httpOnlyCookieExists(existingCookies, soup_cookie_get_name(cookie.get()), soup_cookie_get_path(cookie.get())))
322 continue;
323
324 soup_cookie_jar_add_cookie_with_first_party(jar, firstPartyURI.get(), cookie.release());
325 }
326
327 soup_cookies_free(existingCookies);
328}
329
330void NetworkStorageSession::setCookies(const Vector<Cookie>& cookies, const URL&, const URL&)
331{
332 for (auto cookie : cookies)
333 soup_cookie_jar_add_cookie(cookieStorage(), cookie.toSoupCookie());
334}
335
336void NetworkStorageSession::setCookie(const Cookie& cookie)
337{
338 soup_cookie_jar_add_cookie(cookieStorage(), cookie.toSoupCookie());
339}
340
341void NetworkStorageSession::deleteCookie(const Cookie& cookie)
342{
343 GUniquePtr<SoupCookie> targetCookie(cookie.toSoupCookie());
344 soup_cookie_jar_delete_cookie(cookieStorage(), targetCookie.get());
345}
346
347void NetworkStorageSession::deleteCookie(const URL& url, const String& name) const
348{
349 GUniquePtr<SoupURI> uri = urlToSoupURI(url);
350 if (!uri)
351 return;
352
353 SoupCookieJar* jar = cookieStorage();
354 GUniquePtr<GSList> cookies(soup_cookie_jar_get_cookie_list(jar, uri.get(), TRUE));
355 if (!cookies)
356 return;
357
358 CString cookieName = name.utf8();
359 bool wasDeleted = false;
360 for (GSList* iter = cookies.get(); iter; iter = g_slist_next(iter)) {
361 SoupCookie* cookie = static_cast<SoupCookie*>(iter->data);
362 if (!wasDeleted && cookieName == cookie->name) {
363 soup_cookie_jar_delete_cookie(jar, cookie);
364 wasDeleted = true;
365 }
366 soup_cookie_free(cookie);
367 }
368}
369
370void NetworkStorageSession::deleteAllCookies()
371{
372 SoupCookieJar* cookieJar = cookieStorage();
373 GUniquePtr<GSList> cookies(soup_cookie_jar_all_cookies(cookieJar));
374 for (GSList* item = cookies.get(); item; item = g_slist_next(item)) {
375 SoupCookie* cookie = static_cast<SoupCookie*>(item->data);
376 soup_cookie_jar_delete_cookie(cookieJar, cookie);
377 soup_cookie_free(cookie);
378 }
379}
380
381void NetworkStorageSession::deleteAllCookiesModifiedSince(WallTime timestamp)
382{
383 // FIXME: Add support for deleting cookies modified since the given timestamp. It should probably be added to libsoup.
384 if (timestamp == WallTime::fromRawSeconds(0))
385 deleteAllCookies();
386 else
387 g_warning("Deleting cookies modified since a given time span is not supported yet");
388}
389
390void NetworkStorageSession::deleteCookiesForHostnames(const Vector<String>& hostnames, IncludeHttpOnlyCookies includeHttpOnlyCookies)
391{
392 // FIXME: Not yet implemented.
393 UNUSED_PARAM(includeHttpOnlyCookies);
394 deleteCookiesForHostnames(hostnames);
395}
396
397void NetworkStorageSession::deleteCookiesForHostnames(const Vector<String>& hostnames)
398{
399 SoupCookieJar* cookieJar = cookieStorage();
400
401 for (const auto& hostname : hostnames) {
402 CString hostNameString = hostname.utf8();
403
404 GUniquePtr<GSList> cookies(soup_cookie_jar_all_cookies(cookieJar));
405 for (GSList* item = cookies.get(); item; item = g_slist_next(item)) {
406 SoupCookie* cookie = static_cast<SoupCookie*>(item->data);
407 if (soup_cookie_domain_matches(cookie, hostNameString.data()))
408 soup_cookie_jar_delete_cookie(cookieJar, cookie);
409 soup_cookie_free(cookie);
410 }
411 }
412}
413
414void NetworkStorageSession::getHostnamesWithCookies(HashSet<String>& hostnames)
415{
416 GUniquePtr<GSList> cookies(soup_cookie_jar_all_cookies(cookieStorage()));
417 for (GSList* item = cookies.get(); item; item = g_slist_next(item)) {
418 SoupCookie* cookie = static_cast<SoupCookie*>(item->data);
419 if (cookie->domain)
420 hostnames.add(String::fromUTF8(cookie->domain));
421 soup_cookie_free(cookie);
422 }
423}
424
425Vector<Cookie> NetworkStorageSession::getAllCookies()
426{
427 // FIXME: Implement for WK2 to use.
428 return { };
429}
430
431Vector<Cookie> NetworkStorageSession::getCookies(const URL& url)
432{
433 Vector<Cookie> cookies;
434 GUniquePtr<SoupURI> uri = urlToSoupURI(url);
435 if (!uri)
436 return cookies;
437
438 GUniquePtr<GSList> cookiesList(soup_cookie_jar_get_cookie_list(cookieStorage(), uri.get(), TRUE));
439 for (GSList* item = cookiesList.get(); item; item = g_slist_next(item)) {
440 GUniquePtr<SoupCookie> soupCookie(static_cast<SoupCookie*>(item->data));
441 cookies.append(WebCore::Cookie(soupCookie.get()));
442 }
443
444 return cookies;
445}
446
447bool NetworkStorageSession::getRawCookies(const URL& firstParty, const SameSiteInfo&, const URL& url, Optional<uint64_t> frameID, Optional<uint64_t> pageID, Vector<Cookie>& rawCookies) const
448{
449 UNUSED_PARAM(firstParty);
450 UNUSED_PARAM(frameID);
451 UNUSED_PARAM(pageID);
452 rawCookies.clear();
453 GUniquePtr<SoupURI> uri = urlToSoupURI(url);
454 if (!uri)
455 return false;
456
457 GUniquePtr<GSList> cookies(soup_cookie_jar_get_cookie_list(cookieStorage(), uri.get(), TRUE));
458 if (!cookies)
459 return false;
460
461 for (GSList* iter = cookies.get(); iter; iter = g_slist_next(iter)) {
462 SoupCookie* soupCookie = static_cast<SoupCookie*>(iter->data);
463 Cookie cookie;
464 cookie.name = String::fromUTF8(soupCookie->name);
465 cookie.value = String::fromUTF8(soupCookie->value);
466 cookie.domain = String::fromUTF8(soupCookie->domain);
467 cookie.path = String::fromUTF8(soupCookie->path);
468 cookie.created = 0;
469 cookie.expires = soupCookie->expires ? static_cast<double>(soup_date_to_time_t(soupCookie->expires)) * 1000 : 0;
470 cookie.httpOnly = soupCookie->http_only;
471 cookie.secure = soupCookie->secure;
472 cookie.session = !soupCookie->expires;
473 rawCookies.append(WTFMove(cookie));
474 soup_cookie_free(soupCookie);
475 }
476
477 return true;
478}
479
480static std::pair<String, bool> cookiesForSession(const NetworkStorageSession& session, const URL& url, bool forHTTPHeader, IncludeSecureCookies includeSecureCookies)
481{
482 GUniquePtr<SoupURI> uri = urlToSoupURI(url);
483 if (!uri)
484 return { { }, false };
485
486 GSList* cookies = soup_cookie_jar_get_cookie_list(session.cookieStorage(), uri.get(), forHTTPHeader);
487 bool didAccessSecureCookies = false;
488
489 // libsoup should omit secure cookies itself if the protocol is not https.
490 if (url.protocolIs("https")) {
491 GSList* item = cookies;
492 while (item) {
493 auto cookie = static_cast<SoupCookie*>(item->data);
494 if (soup_cookie_get_secure(cookie)) {
495 didAccessSecureCookies = true;
496 if (includeSecureCookies == IncludeSecureCookies::No) {
497 GSList* next = item->next;
498 soup_cookie_free(static_cast<SoupCookie*>(item->data));
499 cookies = g_slist_remove_link(cookies, item);
500 item = next;
501 continue;
502 }
503 }
504 item = item->next;
505 }
506 }
507
508 if (!cookies)
509 return { { }, false };
510
511 GUniquePtr<char> cookieHeader(soup_cookies_to_cookie_header(cookies));
512 soup_cookies_free(cookies);
513
514 return { String::fromUTF8(cookieHeader.get()), didAccessSecureCookies };
515}
516
517std::pair<String, bool> NetworkStorageSession::cookiesForDOM(const URL& firstParty, const SameSiteInfo&, const URL& url, Optional<uint64_t> frameID, Optional<uint64_t> pageID, IncludeSecureCookies includeSecureCookies) const
518{
519 UNUSED_PARAM(firstParty);
520 UNUSED_PARAM(frameID);
521 UNUSED_PARAM(pageID);
522 return cookiesForSession(*this, url, false, includeSecureCookies);
523}
524
525std::pair<String, bool> NetworkStorageSession::cookieRequestHeaderFieldValue(const URL& firstParty, const SameSiteInfo&, const URL& url, Optional<uint64_t> frameID, Optional<uint64_t> pageID, IncludeSecureCookies includeSecureCookies) const
526{
527 UNUSED_PARAM(firstParty);
528 UNUSED_PARAM(frameID);
529 UNUSED_PARAM(pageID);
530 // Secure cookies will still only be included if url's protocol is https.
531 return cookiesForSession(*this, url, true, includeSecureCookies);
532}
533
534std::pair<String, bool> NetworkStorageSession::cookieRequestHeaderFieldValue(const CookieRequestHeaderFieldProxy& headerFieldProxy) const
535{
536 return cookieRequestHeaderFieldValue(headerFieldProxy.firstParty, headerFieldProxy.sameSiteInfo, headerFieldProxy.url, headerFieldProxy.frameID, headerFieldProxy.pageID, headerFieldProxy.includeSecureCookies);
537}
538
539void NetworkStorageSession::flushCookieStore()
540{
541 // FIXME: Implement for WK2 to use.
542}
543
544} // namespace WebCore
545
546#endif // USE(SOUP)
547