Changeset 249192 in webkit
- Timestamp:
- Aug 28, 2019 3:15:36 AM (5 years ago)
- Location:
- trunk
- Files:
-
- 29 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/Source/WebCore/ChangeLog
r249191 r249192 1 2019-08-28 Claudio Saavedra <csaavedra@igalia.com> 2 3 [GTK][WPE] Implement HSTS for the soup network backend 4 https://bugs.webkit.org/show_bug.cgi?id=192074 5 6 Reviewed by Carlos Garcia Campos. 7 8 libsoup 2.67.1 introduced HSTS support via a SoupSessionFeature. 9 Add support to the soup network backend by adding the feature to 10 SoupNetworkSession and handling HSTS protocol upgrades, by 11 propagating the scheme change further to clients. This patch adds 12 the HSTS feature unconditionally, but it still possible to add 13 a boolean property to the web context class if desired. 14 15 Additionally, add API to the WebKitWebsiteDataManager to specify 16 the directory where the HSTS database is saved. If the directory 17 is not set or if the data manager is ephemeral, use a 18 non-persistent, memory only HSTS enforcer. 19 20 Implement as well the methods needed to clean-up and delete HSTS 21 policies from the storage and expose the feature in GTK+ MiniBrowser's 22 about:data. 23 24 * platform/network/soup/GUniquePtrSoup.h: 25 * platform/network/soup/SoupNetworkSession.cpp: 26 (WebCore::hstsStorageDirectory): 27 (WebCore::SoupNetworkSession::SoupNetworkSession): 28 (WebCore::SoupNetworkSession::setHSTSPersistentStorage): 29 (WebCore::SoupNetworkSession::setupHSTSEnforcer): 30 (WebCore::SoupNetworkSession::getHostNamesWithHSTSCache): 31 (WebCore::SoupNetworkSession::deleteHSTSCacheForHostNames): 32 (WebCore::SoupNetworkSession::clearHSTSCache): 33 * platform/network/soup/SoupNetworkSession.h: 34 1 35 2019-08-28 Said Abou-Hallawa <sabouhallawa@apple.com> 2 36 -
trunk/Source/WebCore/platform/network/soup/GUniquePtrSoup.h
r238851 r249192 33 33 WTF_DEFINE_GPTR_DELETER(SoupMessageHeaders, soup_message_headers_free) 34 34 WTF_DEFINE_GPTR_DELETER(SoupBuffer, soup_buffer_free) 35 #if SOUP_CHECK_VERSION(2, 67, 1) 36 WTF_DEFINE_GPTR_DELETER(SoupHSTSPolicy, soup_hsts_policy_free) 37 #endif 35 38 36 39 } // namespace WTF -
trunk/Source/WebCore/platform/network/soup/SoupNetworkSession.cpp
r248102 r249192 60 60 } 61 61 62 static CString& hstsStorageDirectory() 63 { 64 static NeverDestroyed<CString> directory; 65 return directory.get(); 66 } 67 62 68 #if !LOG_DISABLED 63 69 inline static void soupLogPrinter(SoupLogger*, SoupLoggerLogLevel, char direction, const char* data, gpointer) … … 109 115 SoupNetworkSession::SoupNetworkSession(PAL::SessionID sessionID) 110 116 : m_soupSession(adoptGRef(soup_session_new())) 117 , m_sessionID(sessionID) 111 118 { 112 119 // Values taken from http://www.browserscope.org/ following … … 133 140 setAcceptLanguages(initialAcceptLanguages()); 134 141 135 if (soup_auth_negotiate_supported() && ! sessionID.isEphemeral()) {142 if (soup_auth_negotiate_supported() && !m_sessionID.isEphemeral()) { 136 143 g_object_set(m_soupSession.get(), 137 144 SOUP_SESSION_ADD_FEATURE_BY_TYPE, SOUP_TYPE_AUTH_NEGOTIATE, … … 142 149 setupProxy(); 143 150 setupLogger(); 151 setupHSTSEnforcer(); 144 152 } 145 153 … … 168 176 { 169 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("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 170 260 } 171 261 -
trunk/Source/WebCore/platform/network/soup/SoupNetworkSession.h
r248010 r249192 58 58 SoupCookieJar* cookieJar() const; 59 59 60 static void setHSTSPersistentStorage(const CString& hstsStorageDirectory); 61 void setupHSTSEnforcer(); 62 60 63 static void clearOldSoupCache(const String& cacheDirectory); 61 64 … … 73 76 void setupCustomProtocols(); 74 77 78 void getHostNamesWithHSTSCache(HashSet<String>&); 79 void deleteHSTSCacheForHostNames(const Vector<String>&); 80 void clearHSTSCache(WallTime); 81 75 82 private: 76 83 void setupLogger(); 77 84 78 85 GRefPtr<SoupSession> m_soupSession; 86 PAL::SessionID m_sessionID; 79 87 }; 80 88 -
trunk/Source/WebKit/ChangeLog
r249175 r249192 1 2019-08-28 Claudio Saavedra <csaavedra@igalia.com> 2 3 [GTK][WPE] Implement HSTS for the soup network backend 4 https://bugs.webkit.org/show_bug.cgi?id=192074 5 6 Reviewed by Carlos Garcia Campos. 7 8 libsoup 2.67.1 introduced HSTS support via a SoupSessionFeature. 9 Add support to the soup network backend by adding the feature to 10 SoupNetworkSession and handling HSTS protocol upgrades, by 11 propagating the scheme change further to clients. This patch adds 12 the HSTS feature unconditionally, but it still possible to add 13 a boolean property to the web context class if desired. 14 15 Additionally, add API to the WebKitWebsiteDataManager to specify 16 the directory where the HSTS database is saved. If the directory 17 is not set or if the data manager is ephemeral, use a 18 non-persistent, memory only HSTS enforcer. 19 20 Implement as well the methods needed to clean-up and delete HSTS 21 policies from the storage and expose the feature in GTK+ 22 MiniBrowser's about:data. 23 24 * NetworkProcess/NetworkProcess.cpp: 25 (WebKit::NetworkProcess::fetchWebsiteData): 26 (WebKit::NetworkProcess::deleteWebsiteData): 27 (WebKit::NetworkProcess::deleteWebsiteDataForOrigins): 28 (WebKit::NetworkProcess::deleteWebsiteDataForRegistrableDomains): 29 (WebKit::NetworkProcess::registrableDomainsWithWebsiteData): 30 * NetworkProcess/NetworkProcess.h: 31 (WebKit::NetworkProcess::suppressesConnectionTerminationOnSystemChange const): 32 * NetworkProcess/soup/NetworkDataTaskSoup.cpp: 33 (WebKit::NetworkDataTaskSoup::createRequest): 34 (WebKit::NetworkDataTaskSoup::clearRequest): 35 (WebKit::NetworkDataTaskSoup::shouldAllowHSTSPolicySetting const): 36 (WebKit::NetworkDataTaskSoup::shouldAllowHSTSProtocolUpgrade const): 37 (WebKit::NetworkDataTaskSoup::protocolUpgradedViaHSTS): 38 (WebKit::NetworkDataTaskSoup::hstsEnforced): 39 * NetworkProcess/soup/NetworkDataTaskSoup.h: 40 * NetworkProcess/soup/NetworkProcessSoup.cpp: 41 (WebKit::NetworkProcess::getHostNamesWithHSTSCache): 42 (WebKit::NetworkProcess::deleteHSTSCacheForHostNames): 43 (WebKit::NetworkProcess::clearHSTSCache): 44 (WebKit::NetworkProcess::platformInitializeNetworkProcess): 45 * UIProcess/API/APIWebsiteDataStore.h: 46 * UIProcess/API/glib/APIWebsiteDataStoreGLib.cpp: 47 (API::WebsiteDataStore::defaultHSTSDirectory): 48 * UIProcess/API/glib/WebKitWebContext.cpp: 49 (webkitWebContextConstructed): 50 * UIProcess/API/glib/WebKitWebsiteData.cpp: 51 (recordContainsSupportedDataTypes): 52 (toWebKitWebsiteDataTypes): 53 * UIProcess/API/glib/WebKitWebsiteDataManager.cpp: 54 (webkitWebsiteDataManagerGetProperty): 55 (webkitWebsiteDataManagerSetProperty): 56 (webkitWebsiteDataManagerConstructed): 57 (webkit_website_data_manager_class_init): 58 (webkitWebsiteDataManagerGetDataStore): 59 (webkit_website_data_manager_get_hsts_cache_directory): 60 (toWebsiteDataTypes): 61 * UIProcess/API/gtk/WebKitWebsiteData.h: 62 * UIProcess/API/gtk/WebKitWebsiteDataManager.h: 63 * UIProcess/API/gtk/docs/webkit2gtk-4.0-sections.txt: 64 * UIProcess/API/wpe/WebKitWebsiteData.h: 65 * UIProcess/API/wpe/WebKitWebsiteDataManager.h: 66 * UIProcess/API/wpe/docs/wpe-1.0-sections.txt: 67 * UIProcess/WebsiteData/WebsiteDataStoreConfiguration.cpp: 68 (WebKit::WebsiteDataStoreConfiguration::copy): 69 * UIProcess/WebsiteData/WebsiteDataStoreConfiguration.h: 70 (WebKit::WebsiteDataStoreConfiguration::hstsStorageDirectory const): 71 (WebKit::WebsiteDataStoreConfiguration::setHSTSStorageDirectory): 72 1 73 2019-08-27 Mark Lam <mark.lam@apple.com> 2 74 -
trunk/Source/WebKit/NetworkProcess/NetworkProcess.cpp
r248959 r249192 1350 1350 } 1351 1351 1352 #if PLATFORM(COCOA) 1352 #if PLATFORM(COCOA) || USE(SOUP) 1353 1353 if (websiteDataTypes.contains(WebsiteDataType::HSTSCache)) { 1354 1354 if (auto* networkStorageSession = storageSession(sessionID)) … … 1390 1390 void NetworkProcess::deleteWebsiteData(PAL::SessionID sessionID, OptionSet<WebsiteDataType> websiteDataTypes, WallTime modifiedSince, uint64_t callbackID) 1391 1391 { 1392 #if PLATFORM(COCOA) 1392 #if PLATFORM(COCOA) || USE(SOUP) 1393 1393 if (websiteDataTypes.contains(WebsiteDataType::HSTSCache)) { 1394 1394 if (auto* networkStorageSession = storageSession(sessionID)) … … 1491 1491 } 1492 1492 1493 #if PLATFORM(COCOA) 1493 #if PLATFORM(COCOA) || USE(SOUP) 1494 1494 if (websiteDataTypes.contains(WebsiteDataType::HSTSCache)) { 1495 1495 if (auto* networkStorageSession = storageSession(sessionID)) … … 1673 1673 1674 1674 Vector<String> hostnamesWithHSTSToDelete; 1675 #if PLATFORM(COCOA) 1675 #if PLATFORM(COCOA) || USE(SOUP) 1676 1676 if (websiteDataTypes.contains(WebsiteDataType::HSTSCache)) { 1677 1677 if (auto* networkStorageSession = storageSession(sessionID)) { … … 1854 1854 1855 1855 Vector<String> hostnamesWithHSTSToDelete; 1856 #if PLATFORM(COCOA) 1856 #if PLATFORM(COCOA) || USE(SOUP) 1857 1857 if (websiteDataTypes.contains(WebsiteDataType::HSTSCache)) { 1858 1858 if (auto* networkStorageSession = storageSession(sessionID)) -
trunk/Source/WebKit/NetworkProcess/NetworkProcess.h
r248956 r249192 194 194 #if PLATFORM(COCOA) 195 195 RetainPtr<CFDataRef> sourceApplicationAuditData() const; 196 bool suppressesConnectionTerminationOnSystemChange() const { return m_suppressesConnectionTerminationOnSystemChange; } 197 #endif 198 #if PLATFORM(COCOA) || USE(SOUP) 196 199 void getHostNamesWithHSTSCache(WebCore::NetworkStorageSession&, HashSet<String>&); 197 200 void deleteHSTSCacheForHostNames(WebCore::NetworkStorageSession&, const Vector<String>&); 198 201 void clearHSTSCache(WebCore::NetworkStorageSession&, WallTime modifiedSince); 199 bool suppressesConnectionTerminationOnSystemChange() const { return m_suppressesConnectionTerminationOnSystemChange; }200 202 #endif 201 203 -
trunk/Source/WebKit/NetworkProcess/soup/NetworkDataTaskSoup.cpp
r248846 r249192 39 39 #include <WebCore/MIMETypeRegistry.h> 40 40 #include <WebCore/NetworkStorageSession.h> 41 #include <WebCore/PublicSuffix.h> 41 42 #include <WebCore/SharedBuffer.h> 42 43 #include <WebCore/SoupNetworkSession.h> … … 147 148 #endif 148 149 } 150 151 #if SOUP_CHECK_VERSION(2, 67, 1) 152 if ((m_currentRequest.url().protocolIs("https") && !shouldAllowHSTSPolicySetting()) || (m_currentRequest.url().protocolIs("http") && !shouldAllowHSTSProtocolUpgrade())) 153 soup_message_disable_feature(soupMessage.get(), SOUP_TYPE_HSTS_ENFORCER); 154 else 155 g_signal_connect(soup_session_get_feature(static_cast<NetworkSessionSoup&>(*m_session).soupSession(), SOUP_TYPE_HSTS_ENFORCER), "hsts-enforced", G_CALLBACK(hstsEnforced), this); 156 #endif 149 157 150 158 // Make sure we have an Accept header for subresources; some sites want this to serve some of their subresources. … … 193 201 m_soupMessage = nullptr; 194 202 } 195 if (m_session) 203 if (m_session) { 196 204 g_signal_handlers_disconnect_matched(static_cast<NetworkSessionSoup&>(*m_session).soupSession(), G_SIGNAL_MATCH_DATA, 0, 0, nullptr, nullptr, this); 205 #if SOUP_CHECK_VERSION(2, 67, 1) 206 g_signal_handlers_disconnect_by_data(soup_session_get_feature(static_cast<NetworkSessionSoup&>(*m_session).soupSession(), SOUP_TYPE_HSTS_ENFORCER), this); 207 #endif 208 } 197 209 } 198 210 … … 1061 1073 } 1062 1074 1075 #if SOUP_CHECK_VERSION(2, 67, 1) 1076 bool NetworkDataTaskSoup::shouldAllowHSTSPolicySetting() const 1077 { 1078 // Follow Apple's HSTS abuse mitigation 1: 1079 // "Limit HSTS State to the Hostname, or the Top Level Domain + 1" 1080 if (isTopLevelNavigation() || hostsAreEqual(m_currentRequest.url(), m_currentRequest.firstPartyForCookies()) || isPublicSuffix(m_currentRequest.url().host().toStringWithoutCopying())) 1081 return true; 1082 1083 return false; 1084 } 1085 1086 bool NetworkDataTaskSoup::shouldAllowHSTSProtocolUpgrade() const 1087 { 1088 // Follow Apple's HSTS abuse mitgation 2: 1089 // "Ignore HSTS State for Subresource Requests to Blocked Domains" 1090 if (!isTopLevelNavigation() && !m_currentRequest.allowCookies()) 1091 return false; 1092 1093 return true; 1094 } 1095 1096 void NetworkDataTaskSoup::protocolUpgradedViaHSTS(SoupMessage* soupMessage) 1097 { 1098 m_response = ResourceResponse::syntheticRedirectResponse(m_currentRequest.url(), soupURIToURL(soup_message_get_uri(soupMessage))); 1099 continueHTTPRedirection(); 1100 } 1101 1102 void NetworkDataTaskSoup::hstsEnforced(SoupHSTSEnforcer*, SoupMessage* soupMessage, NetworkDataTaskSoup* task) 1103 { 1104 if (task->state() == State::Canceling || task->state() == State::Completed || !task->m_client) { 1105 task->clearRequest(); 1106 return; 1107 } 1108 1109 if (soupMessage == task->m_soupMessage.get()) 1110 task->protocolUpgradedViaHSTS(soupMessage); 1111 } 1112 #endif 1113 1063 1114 void NetworkDataTaskSoup::didStartRequest() 1064 1115 { -
trunk/Source/WebKit/NetworkProcess/soup/NetworkDataTaskSoup.h
r241317 r249192 113 113 static void requestStartedCallback(SoupSession*, SoupMessage*, SoupSocket*, NetworkDataTaskSoup*); 114 114 #endif 115 #if SOUP_CHECK_VERSION(2, 67, 1) 116 bool shouldAllowHSTSPolicySetting() const; 117 bool shouldAllowHSTSProtocolUpgrade() const; 118 void protocolUpgradedViaHSTS(SoupMessage*); 119 static void hstsEnforced(SoupHSTSEnforcer*, SoupMessage*, NetworkDataTaskSoup*); 120 #endif 115 121 void didStartRequest(); 116 122 static void restartedCallback(SoupMessage*, NetworkDataTaskSoup*); -
trunk/Source/WebKit/NetworkProcess/soup/NetworkProcessSoup.cpp
r248999 r249192 96 96 } 97 97 98 void NetworkProcess::getHostNamesWithHSTSCache(WebCore::NetworkStorageSession& storageSession, HashSet<String>& hostNames) 99 { 100 const auto* session = static_cast<NetworkSessionSoup*>(networkSession(storageSession.sessionID())); 101 session->soupNetworkSession().getHostNamesWithHSTSCache(hostNames); 102 } 103 104 void NetworkProcess::deleteHSTSCacheForHostNames(WebCore::NetworkStorageSession& storageSession, const Vector<String>& hostNames) 105 { 106 const auto* session = static_cast<NetworkSessionSoup*>(networkSession(storageSession.sessionID())); 107 session->soupNetworkSession().deleteHSTSCacheForHostNames(hostNames); 108 } 109 110 void NetworkProcess::clearHSTSCache(WebCore::NetworkStorageSession& storageSession, WallTime modifiedSince) 111 { 112 const auto* session = static_cast<NetworkSessionSoup*>(networkSession(storageSession.sessionID())); 113 session->soupNetworkSession().clearHSTSCache(modifiedSince); 114 } 115 98 116 void NetworkProcess::userPreferredLanguagesChanged(const Vector<String>& languages) 99 117 { … … 130 148 131 149 setIgnoreTLSErrors(parameters.ignoreTLSErrors); 150 151 if (!parameters.hstsStorageDirectory.isEmpty()) 152 SoupNetworkSession::setHSTSPersistentStorage(parameters.hstsStorageDirectory.utf8()); 153 forEachNetworkSession([](const auto& session) { 154 static_cast<const NetworkSessionSoup&>(session).soupNetworkSession().setupHSTSEnforcer(); 155 }); 132 156 } 133 157 -
trunk/Source/WebKit/UIProcess/API/APIWebsiteDataStore.h
r242203 r249192 67 67 static WTF::String defaultDeviceIdHashSaltsStorageDirectory(); 68 68 static WTF::String defaultWebSQLDatabaseDirectory(); 69 #if USE(GLIB) 70 static WTF::String defaultHSTSDirectory(); 71 #endif 69 72 static WTF::String defaultResourceLoadStatisticsDirectory(); 70 73 static WTF::String defaultJavaScriptConfigurationDirectory(); -
trunk/Source/WebKit/UIProcess/API/glib/APIWebsiteDataStoreGLib.cpp
r246790 r249192 83 83 { 84 84 return websiteDataDirectoryFileSystemRepresentation(BASE_DIRECTORY G_DIR_SEPARATOR_S "databases"); 85 } 86 87 WTF::String WebsiteDataStore::defaultHSTSDirectory() 88 { 89 return websiteDataDirectoryFileSystemRepresentation(BASE_DIRECTORY G_DIR_SEPARATOR_S); 85 90 } 86 91 -
trunk/Source/WebKit/UIProcess/API/glib/WebKitWebContext.cpp
r248846 r249192 342 342 configuration.setApplicationCacheDirectory(FileSystem::stringFromFileSystemRepresentation(webkit_website_data_manager_get_offline_application_cache_directory(priv->websiteDataManager.get()))); 343 343 configuration.setIndexedDBDatabaseDirectory(FileSystem::stringFromFileSystemRepresentation(webkit_website_data_manager_get_indexeddb_directory(priv->websiteDataManager.get()))); 344 configuration.setHSTSStorageDirectory(FileSystem::stringFromFileSystemRepresentation(webkit_website_data_manager_get_hsts_cache_directory(priv->websiteDataManager.get()))); 344 345 ALLOW_DEPRECATED_DECLARATIONS_BEGIN 345 346 configuration.setWebSQLDatabaseDirectory(FileSystem::stringFromFileSystemRepresentation(webkit_website_data_manager_get_websql_directory(priv->websiteDataManager.get()))); -
trunk/Source/WebKit/UIProcess/API/glib/WebKitWebsiteData.cpp
r237031 r249192 74 74 WebsiteDataType::WebSQLDatabases, 75 75 WebsiteDataType::IndexedDBDatabases, 76 WebsiteDataType::HSTSCache, 76 77 #if ENABLE(NETSCAPE_PLUGIN_API) 77 78 WebsiteDataType::PlugInData, … … 99 100 if (types.contains(WebsiteDataType::IndexedDBDatabases)) 100 101 returnValue |= WEBKIT_WEBSITE_DATA_INDEXEDDB_DATABASES; 102 if (types.contains(WebsiteDataType::HSTSCache)) 103 returnValue |= WEBKIT_WEBSITE_DATA_HSTS_CACHE; 101 104 #if ENABLE(NETSCAPE_PLUGIN_API) 102 105 if (types.contains(WebsiteDataType::PlugInData)) -
trunk/Source/WebKit/UIProcess/API/glib/WebKitWebsiteDataManager.cpp
r248293 r249192 85 85 PROP_INDEXEDDB_DIRECTORY, 86 86 PROP_WEBSQL_DIRECTORY, 87 PROP_HSTS_CACHE_DIRECTORY, 87 88 PROP_IS_EPHEMERAL 88 89 }; … … 102 103 GUniquePtr<char> indexedDBDirectory; 103 104 GUniquePtr<char> webSQLDirectory; 105 GUniquePtr<char> hstsCacheDirectory; 104 106 105 107 GRefPtr<WebKitCookieManager> cookieManager; … … 137 139 ALLOW_DEPRECATED_DECLARATIONS_END 138 140 break; 141 case PROP_HSTS_CACHE_DIRECTORY: 142 g_value_set_string(value, webkit_website_data_manager_get_hsts_cache_directory(manager)); 143 break; 139 144 case PROP_IS_EPHEMERAL: 140 145 g_value_set_boolean(value, webkit_website_data_manager_is_ephemeral(manager)); … … 170 175 case PROP_WEBSQL_DIRECTORY: 171 176 manager->priv->webSQLDirectory.reset(g_value_dup_string(value)); 177 break; 178 case PROP_HSTS_CACHE_DIRECTORY: 179 manager->priv->hstsCacheDirectory.reset(g_value_dup_string(value)); 172 180 break; 173 181 case PROP_IS_EPHEMERAL: … … 199 207 if (!priv->applicationCacheDirectory) 200 208 priv->applicationCacheDirectory.reset(g_build_filename(priv->baseCacheDirectory.get(), "applications", nullptr)); 209 if (!priv->hstsCacheDirectory) 210 priv->hstsCacheDirectory.reset(g_strdup(priv->baseCacheDirectory.get())); 201 211 } 202 212 } … … 334 344 335 345 /** 346 * WebKitWebsiteDataManager:hsts-cache-directory: 347 * 348 * The directory where the HTTP Strict-Transport-Security (HSTS) cache will be stored. 349 * 350 * Since: 2.26 351 */ 352 g_object_class_install_property( 353 gObjectClass, 354 PROP_HSTS_CACHE_DIRECTORY, 355 g_param_spec_string( 356 "hsts-cache-directory", 357 _("HSTS Cache Directory"), 358 _("The directory where the HTTP Strict-Transport-Security cache will be stored"), 359 nullptr, 360 static_cast<GParamFlags>(WEBKIT_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY))); 361 362 /** 336 363 * WebKitWebsiteDataManager:is-ephemeral: 337 364 * … … 375 402 configuration->setWebSQLDatabaseDirectory(!priv->webSQLDirectory ? 376 403 API::WebsiteDataStore::defaultWebSQLDatabaseDirectory() : FileSystem::stringFromFileSystemRepresentation(priv->webSQLDirectory.get())); 404 configuration->setHSTSStorageDirectory(!priv->hstsCacheDirectory ? 405 API::WebsiteDataStore::defaultHSTSDirectory() : FileSystem::stringFromFileSystemRepresentation(priv->hstsCacheDirectory.get())); 377 406 configuration->setMediaKeysStorageDirectory(API::WebsiteDataStore::defaultMediaKeysStorageDirectory()); 378 407 priv->websiteDataStore = API::WebsiteDataStore::createLegacy(WTFMove(configuration)); … … 611 640 priv->webSQLDirectory.reset(g_strdup(API::WebsiteDataStore::defaultWebSQLDatabaseDirectory().utf8().data())); 612 641 return priv->webSQLDirectory.get(); 642 } 643 644 /** 645 * webkit_website_data_manager_get_hsts_cache_directory: 646 * @manager: a #WebKitWebsiteDataManager 647 * 648 * Get the #WebKitWebsiteDataManager:hsts-cache-directory property. 649 * 650 * Returns: (allow-none): the directory where the HSTS cache is stored or %NULL if @manager is ephemeral. 651 * 652 * Since: 2.26 653 */ 654 const gchar* webkit_website_data_manager_get_hsts_cache_directory(WebKitWebsiteDataManager* manager) 655 { 656 g_return_val_if_fail(WEBKIT_IS_WEBSITE_DATA_MANAGER(manager), nullptr); 657 658 WebKitWebsiteDataManagerPrivate* priv = manager->priv; 659 if (priv->websiteDataStore && !priv->websiteDataStore->isPersistent()) 660 return nullptr; 661 662 if (!priv->hstsCacheDirectory) 663 priv->hstsCacheDirectory.reset(g_strdup(API::WebsiteDataStore::defaultHSTSDirectory().utf8().data())); 664 return priv->hstsCacheDirectory.get(); 613 665 } 614 666 … … 650 702 if (types & WEBKIT_WEBSITE_DATA_INDEXEDDB_DATABASES) 651 703 returnValue.add(WebsiteDataType::IndexedDBDatabases); 704 if (types & WEBKIT_WEBSITE_DATA_HSTS_CACHE) 705 returnValue.add(WebsiteDataType::HSTSCache); 652 706 #if ENABLE(NETSCAPE_PLUGIN_API) 653 707 if (types & WEBKIT_WEBSITE_DATA_PLUGIN_DATA) -
trunk/Source/WebKit/UIProcess/API/gtk/WebKitWebsiteData.h
r246353 r249192 46 46 * @WEBKIT_WEBSITE_DATA_COOKIES: Cookies. 47 47 * @WEBKIT_WEBSITE_DATA_DEVICE_ID_HASH_SALT: Hash salt used to generate the device ids used by webpages. Since 2.24 48 * @WEBKIT_WEBSITE_DATA_HSTS_CACHE: HSTS cache. Since 2.26 48 49 * @WEBKIT_WEBSITE_DATA_ALL: All types. 49 50 * … … 63 64 WEBKIT_WEBSITE_DATA_COOKIES = 1 << 8, 64 65 WEBKIT_WEBSITE_DATA_DEVICE_ID_HASH_SALT = 1 << 9, 65 WEBKIT_WEBSITE_DATA_ALL = (1 << 10) - 1 66 WEBKIT_WEBSITE_DATA_HSTS_CACHE = 1 << 10, 67 WEBKIT_WEBSITE_DATA_ALL = (1 << 11) - 1 66 68 } WebKitWebsiteDataTypes; 67 69 -
trunk/Source/WebKit/UIProcess/API/gtk/WebKitWebsiteDataManager.h
r246353 r249192 91 91 webkit_website_data_manager_get_websql_directory (WebKitWebsiteDataManager *manager); 92 92 93 WEBKIT_API const gchar * 94 webkit_website_data_manager_get_hsts_cache_directory (WebKitWebsiteDataManager *manager); 95 93 96 WEBKIT_API WebKitCookieManager * 94 97 webkit_website_data_manager_get_cookie_manager (WebKitWebsiteDataManager *manager); -
trunk/Source/WebKit/UIProcess/API/gtk/docs/webkit2gtk-4.0-sections.txt
r245176 r249192 1404 1404 webkit_website_data_manager_get_indexeddb_directory 1405 1405 webkit_website_data_manager_get_websql_directory 1406 webkit_website_data_manager_get_hsts_cache_directory 1406 1407 webkit_website_data_manager_get_cookie_manager 1407 1408 webkit_website_data_manager_fetch -
trunk/Source/WebKit/UIProcess/API/wpe/WebKitWebsiteData.h
r246353 r249192 46 46 * @WEBKIT_WEBSITE_DATA_COOKIES: Cookies. 47 47 * @WEBKIT_WEBSITE_DATA_DEVICE_ID_HASH_SALT: Hash salt used to generate the device ids used by webpages. Since 2.24 48 * @WEBKIT_WEBSITE_DATA_HSTS_CACHE: HSTS cache. Since 2.26 48 49 * @WEBKIT_WEBSITE_DATA_ALL: All types. 49 50 * … … 63 64 WEBKIT_WEBSITE_DATA_COOKIES = 1 << 8, 64 65 WEBKIT_WEBSITE_DATA_DEVICE_ID_HASH_SALT = 1 << 9, 65 WEBKIT_WEBSITE_DATA_ALL = (1 << 10) - 1 66 WEBKIT_WEBSITE_DATA_HSTS_CACHE = 1 << 10, 67 WEBKIT_WEBSITE_DATA_ALL = (1 << 11) - 1 66 68 } WebKitWebsiteDataTypes; 67 69 -
trunk/Source/WebKit/UIProcess/API/wpe/WebKitWebsiteDataManager.h
r246353 r249192 91 91 webkit_website_data_manager_get_websql_directory (WebKitWebsiteDataManager *manager); 92 92 93 WEBKIT_API const gchar * 94 webkit_website_data_manager_get_hsts_cache_directory (WebKitWebsiteDataManager *manager); 95 93 96 WEBKIT_API WebKitCookieManager * 94 97 webkit_website_data_manager_get_cookie_manager (WebKitWebsiteDataManager *manager); -
trunk/Source/WebKit/UIProcess/API/wpe/docs/wpe-1.0-sections.txt
r244260 r249192 1278 1278 webkit_website_data_manager_get_indexeddb_directory 1279 1279 webkit_website_data_manager_get_websql_directory 1280 webkit_website_data_manager_get_hsts_cache_directory 1280 1281 webkit_website_data_manager_get_cookie_manager 1281 1282 webkit_website_data_manager_fetch -
trunk/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStoreConfiguration.cpp
r247476 r249192 51 51 copy->m_serviceWorkerRegistrationDirectory = this->m_serviceWorkerRegistrationDirectory; 52 52 copy->m_webSQLDatabaseDirectory = this->m_webSQLDatabaseDirectory; 53 #if USE(GLIB) 54 copy->m_hstsStorageDirectory = this->m_hstsStorageDirectory; 55 #endif 53 56 copy->m_localStorageDirectory = this->m_localStorageDirectory; 54 57 copy->m_mediaKeysStorageDirectory = this->m_mediaKeysStorageDirectory; -
trunk/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStoreConfiguration.h
r247476 r249192 62 62 const String& webSQLDatabaseDirectory() const { return m_webSQLDatabaseDirectory; } 63 63 void setWebSQLDatabaseDirectory(String&& directory) { m_webSQLDatabaseDirectory = WTFMove(directory); } 64 64 #if USE(GLIB) // According to r245075 this will eventually move here. 65 const String& hstsStorageDirectory() const { return m_hstsStorageDirectory; } 66 void setHSTSStorageDirectory(String&& directory) { m_hstsStorageDirectory = WTFMove(directory); } 67 #endif 65 68 const String& localStorageDirectory() const { return m_localStorageDirectory; } 66 69 void setLocalStorageDirectory(String&& directory) { m_localStorageDirectory = WTFMove(directory); } … … 119 122 String m_serviceWorkerRegistrationDirectory; 120 123 String m_webSQLDatabaseDirectory; 124 #if USE(GLIB) 125 String m_hstsStorageDirectory; 126 #endif 121 127 String m_localStorageDirectory; 122 128 String m_mediaKeysStorageDirectory; -
trunk/Tools/ChangeLog
r249190 r249192 1 2019-08-02 Claudio Saavedra <csaavedra@igalia.com> 2 3 [GTK][WPE] Implement HSTS for the soup network backend 4 https://bugs.webkit.org/show_bug.cgi?id=192074 5 6 Reviewed by Carlos Garcia Campos. 7 8 libsoup 2.67.1 introduced HSTS support via a SoupSessionFeature. 9 Add support to the soup network backend by adding the feature to 10 SoupNetworkSession and handling HSTS protocol upgrades, by 11 propagating the scheme change further to clients. This patch adds 12 the HSTS feature unconditionally, but it still possible to add 13 a boolean property to the web context class if desired. 14 15 Additionally, add API to the WebKitWebsiteDataManager to specify 16 the directory where the HSTS database is saved. If the directory 17 is not set or if the data manager is ephemeral, use a 18 non-persistent, memory only HSTS enforcer. 19 20 Implement as well the methods needed to clean-up and delete HSTS 21 policies from the storage and expose the feature in GTK+ 22 MiniBrowser's about:data. 23 24 * MiniBrowser/gtk/main.c: 25 (gotWebsiteDataCallback): 26 * TestWebKitAPI/Tests/WebKitGLib/TestWebsiteData.cpp: 27 (serverCallback): 28 (testWebsiteDataConfiguration): 29 (testWebsiteDataEphemeral): 30 (prepopulateHstsData): 31 (testWebsiteDataHsts): 32 (beforeAll): 33 * TestWebKitAPI/glib/WebKitGLib/TestMain.h: 34 (Test::Test): 35 * gtk/jhbuild.modules: Bump libsoup to 2.67.91 for the new APIs 36 * wpe/jhbuild.modules: Ditto 37 * MiniBrowser/gtk/main.c: 38 (gotWebsiteDataCallback): 39 1 40 2019-08-27 James Darpinian <jdarpinian@google.com> 2 41 -
trunk/Tools/MiniBrowser/gtk/main.c
r249004 r249192 419 419 aboutDataFillTable(result, dataRequest, dataList, "Plugins Data", WEBKIT_WEBSITE_DATA_PLUGIN_DATA, NULL, pageID); 420 420 aboutDataFillTable(result, dataRequest, dataList, "Offline Web Applications Cache", WEBKIT_WEBSITE_DATA_OFFLINE_APPLICATION_CACHE, webkit_website_data_manager_get_offline_application_cache_directory(manager), pageID); 421 aboutDataFillTable(result, dataRequest, dataList, "HSTS Cache", WEBKIT_WEBSITE_DATA_HSTS_CACHE, webkit_website_data_manager_get_hsts_cache_directory(manager), pageID); 421 422 422 423 result = g_string_append(result, "</body></html>"); -
trunk/Tools/TestWebKitAPI/Tests/WebKitGLib/TestWebsiteData.cpp
r248435 r249192 20 20 #include "config.h" 21 21 22 #include <WebCore/GUniquePtrSoup.h> 22 23 #include "WebKitTestServer.h" 23 24 #include "WebViewTest.h" … … 36 37 static const char* emptyHTML = "<html><body></body></html>"; 37 38 soup_message_headers_replace(message->response_headers, "Set-Cookie", "foo=bar; Max-Age=60"); 39 soup_message_headers_replace(message->response_headers, "Strict-Transport-Security", "max-age=3600"); 38 40 soup_message_body_append(message->response_body, SOUP_MEMORY_STATIC, emptyHTML, strlen(emptyHTML)); 39 41 soup_message_body_complete(message->response_body); … … 163 165 g_assert_true(g_file_test(diskCacheDirectory.get(), G_FILE_TEST_IS_DIR)); 164 166 165 // Clear all persistent caches, since the data dir is common to all test cases. 167 GUniquePtr<char> hstsCacheDirectory(g_build_filename(Test::dataDirectory(), "hsts", nullptr)); 168 g_assert_cmpstr(hstsCacheDirectory.get(), ==, webkit_website_data_manager_get_hsts_cache_directory(test->m_manager)); 169 g_assert_true(g_file_test(hstsCacheDirectory.get(), G_FILE_TEST_IS_DIR)); 170 171 // Clear all persistent caches, since the data dir is common to all test cases. Note: not cleaning the HSTS cache here as its data 172 // is needed for the HSTS tests, where data cleaning will be tested. 166 173 static const WebKitWebsiteDataTypes persistentCaches = static_cast<WebKitWebsiteDataTypes>(WEBKIT_WEBSITE_DATA_DISK_CACHE | WEBKIT_WEBSITE_DATA_LOCAL_STORAGE 167 174 | WEBKIT_WEBSITE_DATA_INDEXEDDB_DATABASES | WEBKIT_WEBSITE_DATA_OFFLINE_APPLICATION_CACHE | WEBKIT_WEBSITE_DATA_DEVICE_ID_HASH_SALT); … … 177 184 g_assert_cmpstr(webkit_website_data_manager_get_disk_cache_directory(test->m_manager), !=, webkit_website_data_manager_get_disk_cache_directory(defaultManager)); 178 185 g_assert_cmpstr(webkit_website_data_manager_get_offline_application_cache_directory(test->m_manager), !=, webkit_website_data_manager_get_offline_application_cache_directory(defaultManager)); 186 g_assert_cmpstr(webkit_website_data_manager_get_hsts_cache_directory(test->m_manager), !=, webkit_website_data_manager_get_hsts_cache_directory(defaultManager)); 179 187 180 188 // Using Test::dataDirectory() we get the default configuration but for a differrent prefix. … … 223 231 g_assert_null(webkit_website_data_manager_get_offline_application_cache_directory(manager.get())); 224 232 g_assert_null(webkit_website_data_manager_get_indexeddb_directory(manager.get())); 233 g_assert_null(webkit_website_data_manager_get_hsts_cache_directory(manager.get())); 225 234 226 235 // Configuration is ignored when is-ephemeral is used. … … 484 493 } 485 494 495 #if SOUP_CHECK_VERSION(2, 67, 91) 496 static void prepopulateHstsData() 497 { 498 // HSTS headers will be ignored in this test because the spec forbids STS policies from being honored for hosts with 499 // an IP address instead of a domain. In order to be able to test the data manager API with HSTS website data, we 500 // prepopulate the HSTS storage using the libsoup API directly. 501 502 GUniquePtr<char> hstsCacheDirectory(g_build_filename(Test::dataDirectory(), "hsts", nullptr)); 503 GUniquePtr<char> hstsDatabase(g_build_filename(hstsCacheDirectory.get(), "hsts-storage.sqlite", nullptr)); 504 g_mkdir_with_parents(hstsCacheDirectory.get(), 0700); 505 506 GRefPtr<SoupHSTSEnforcer> enforcer = adoptGRef(soup_hsts_enforcer_db_new(hstsDatabase.get())); 507 GUniquePtr<SoupHSTSPolicy> policy(soup_hsts_policy_new("webkitgtk.org", 3600, true)); 508 soup_hsts_enforcer_set_policy(enforcer.get(), policy.get()); 509 510 policy.reset(soup_hsts_policy_new("webkit.org", 3600, true)); 511 soup_hsts_enforcer_set_policy(enforcer.get(), policy.get()); 512 } 513 514 static void testWebsiteDataHsts(WebsiteDataTest* test, gconstpointer) 515 { 516 GList* dataList = test->fetch(WEBKIT_WEBSITE_DATA_HSTS_CACHE); 517 g_assert_cmpuint(g_list_length(dataList), ==, 2); 518 WebKitWebsiteData* data = static_cast<WebKitWebsiteData*>(dataList->data); 519 g_assert_cmpuint(webkit_website_data_get_types(data), ==, WEBKIT_WEBSITE_DATA_HSTS_CACHE); 520 // HSTS data size is unknown. 521 g_assert_cmpuint(webkit_website_data_get_size(data, WEBKIT_WEBSITE_DATA_HSTS_CACHE), ==, 0); 522 523 GList removeList = { data, nullptr, nullptr }; 524 test->remove(WEBKIT_WEBSITE_DATA_HSTS_CACHE, &removeList); 525 dataList = test->fetch(WEBKIT_WEBSITE_DATA_HSTS_CACHE); 526 g_assert_cmpuint(g_list_length(dataList), ==, 1); 527 data = static_cast<WebKitWebsiteData*>(dataList->data); 528 g_assert_cmpuint(webkit_website_data_get_types(data), ==, WEBKIT_WEBSITE_DATA_HSTS_CACHE); 529 530 // Remove all HSTS data. 531 test->clear(WEBKIT_WEBSITE_DATA_HSTS_CACHE, 0); 532 g_assert_null(test->fetch(WEBKIT_WEBSITE_DATA_HSTS_CACHE)); 533 } 534 #endif 535 486 536 static void testWebsiteDataCookies(WebsiteDataTest* test, gconstpointer) 487 537 { … … 571 621 kServer = new WebKitTestServer(); 572 622 kServer->run(serverCallback); 623 624 #if SOUP_CHECK_VERSION(2, 67, 91) 625 prepopulateHstsData(); 626 #endif 573 627 574 628 WebsiteDataTest::add("WebKitWebsiteData", "configuration", testWebsiteDataConfiguration); … … 579 633 WebsiteDataTest::add("WebKitWebsiteData", "appcache", testWebsiteDataAppcache); 580 634 WebsiteDataTest::add("WebKitWebsiteData", "cookies", testWebsiteDataCookies); 635 #if SOUP_CHECK_VERSION(2, 67, 91) 636 WebsiteDataTest::add("WebKitWebsiteData", "hsts", testWebsiteDataHsts); 637 #endif 581 638 WebsiteDataTest::add("WebKitWebsiteData", "deviceidhashsalt", testWebsiteDataDeviceIdHashSalt); 582 639 } -
trunk/Tools/TestWebKitAPI/glib/WebKitGLib/TestMain.h
r245618 r249192 122 122 GUniquePtr<char> applicationCacheDirectory(g_build_filename(dataDirectory(), "appcache", nullptr)); 123 123 GUniquePtr<char> webSQLDirectory(g_build_filename(dataDirectory(), "websql", nullptr)); 124 GUniquePtr<char> hstsDirectory(g_build_filename(dataDirectory(), "hsts", nullptr)); 124 125 GRefPtr<WebKitWebsiteDataManager> websiteDataManager = adoptGRef(webkit_website_data_manager_new( 125 126 "local-storage-directory", localStorageDirectory.get(), "indexeddb-directory", indexedDBDirectory.get(), 126 127 "disk-cache-directory", diskCacheDirectory.get(), "offline-application-cache-directory", applicationCacheDirectory.get(), 127 "websql-directory", webSQLDirectory.get(), nullptr));128 "websql-directory", webSQLDirectory.get(), "hsts-cache-directory", hstsDirectory.get(), nullptr)); 128 129 129 130 m_webContext = adoptGRef(webkit_web_context_new_with_website_data_manager(websiteDataManager.get())); -
trunk/Tools/gtk/jhbuild.modules
r248099 r249192 239 239 <dep package="libpsl"/> 240 240 </dependencies> 241 <branch module="/pub/GNOME/sources/libsoup/2.67/libsoup-${version}.tar.xz" version="2.67.9 0"242 repo="ftp.gnome.org" 243 hash="sha256:3 03686002bd7d7bf93204eebe0d5ec9f5d57d071e3411e2304d6f8bcfa97ef79">241 <branch module="/pub/GNOME/sources/libsoup/2.67/libsoup-${version}.tar.xz" version="2.67.91" 242 repo="ftp.gnome.org" 243 hash="sha256:390b5b28263d3bdf9866fa694346caa5e4bcb986e014e3383e9b6130b706f3da"> 244 244 </branch> 245 245 </meson> -
trunk/Tools/wpe/jhbuild.modules
r248767 r249192 101 101 <dep package="libpsl"/> 102 102 </dependencies> 103 <branch module="/pub/GNOME/sources/libsoup/2.67/libsoup-${version}.tar.xz" version="2.67.9 0"104 repo="ftp.gnome.org" 105 hash="sha256:3 03686002bd7d7bf93204eebe0d5ec9f5d57d071e3411e2304d6f8bcfa97ef79">103 <branch module="/pub/GNOME/sources/libsoup/2.67/libsoup-${version}.tar.xz" version="2.67.91" 104 repo="ftp.gnome.org" 105 hash="sha256:390b5b28263d3bdf9866fa694346caa5e4bcb986e014e3383e9b6130b706f3da"> 106 106 </branch> 107 107 </meson>
Note: See TracChangeset
for help on using the changeset viewer.