Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(773)

Side by Side Diff: chrome/browser/ssl/chrome_ssl_host_state_decisions_test.cc

Issue 369703002: Remember user decisions on invalid certificates behind a flag (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Fixed several bot compile errors Created 6 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 // Copyright (c) 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include <stdint.h>
6
7 #include "base/command_line.h"
8 #include "base/strings/string_number_conversions.h"
9 #include "base/test/simple_test_clock.h"
10 #include "chrome/browser/browsing_data/browsing_data_helper.h"
11 #include "chrome/browser/browsing_data/browsing_data_remover.h"
12 #include "chrome/browser/browsing_data/browsing_data_remover_test_util.h"
13 #include "chrome/browser/profiles/profile.h"
14 #include "chrome/browser/ssl/chrome_ssl_host_state_decisions.h"
15 #include "chrome/browser/ui/browser.h"
16 #include "chrome/browser/ui/tabs/tab_strip_model.h"
17 #include "chrome/common/chrome_switches.h"
18 #include "chrome/test/base/in_process_browser_test.h"
19 #include "content/public/browser/ssl_host_state_decisions.h"
20 #include "content/public/browser/web_contents.h"
21 #include "content/public/test/browser_test_utils.h"
22 #include "net/base/test_data_directory.h"
23 #include "net/test/cert_test_util.h"
24 #include "testing/gtest/include/gtest/gtest.h"
25 #include "url/gurl.h"
26
27 namespace {
28
29 const char kGoogleCertFile[] = "google.single.der";
30
31 const char kWWWGoogleURL[] = "https://www.google.com";
32 const char kGoogleURL[] = "https://google.com";
33 const char kExampleURL[] = "https://example.com";
34
35 const char* kForgetAtSessionEnd = "-1";
36 const char* kForgetInstantly = "0";
37 const char* kDeltaSecondsString = "86400";
38 const uint64_t kDeltaOneDayInSeconds = UINT64_C(86400);
39
40 scoped_refptr<net::X509Certificate> GetGoogleCert() {
41 return net::ImportCertFromFile(net::GetTestCertsDirectory(), kGoogleCertFile);
42 }
43
44 } // namespace
45
46 class ChromeSSLHostStateDecisionsTest : public InProcessBrowserTest {};
47
48 // ChromeSSLHostStateDecisionsTest tests basic unit test functionality of the
49 // SSLHostStateDecisions class. For example, tests that if a certificate is
50 // accepted, then it is added to queryable, and if it is revoked, it is not
51 // queryable. Even though it is effectively a unit test, in needs to be an
52 // InProcessBrowserTest because the actual functionality is provided by
53 // ChromeSSLHostStateDecisions which is provided per-profile.
54 //
55 // QueryPolicy unit tests the expected behavior of calling QueryPolicy on the
56 // SSLHostStateDecisions class after various SSL cert decisions have been made.
57 IN_PROC_BROWSER_TEST_F(ChromeSSLHostStateDecisionsTest, QueryPolicy) {
58 GURL www_google_gurl(kWWWGoogleURL);
59 GURL google_gurl(kGoogleURL);
60 GURL example_gurl(kExampleURL);
61 scoped_refptr<net::X509Certificate> google_cert = GetGoogleCert();
62 content::WebContents* tab =
63 browser()->tab_strip_model()->GetActiveWebContents();
64 Profile* profile = Profile::FromBrowserContext(tab->GetBrowserContext());
65 content::SSLHostStateDecisions* state = profile->GetSSLHostStateDecisions();
66
67 // Verifying that all three of the certs we will be looking at are unknown
68 // before any action has been taken.
69 EXPECT_EQ(
70 net::CertPolicy::UNKNOWN,
71 state->QueryPolicy(
72 www_google_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID));
73 EXPECT_EQ(net::CertPolicy::UNKNOWN,
74 state->QueryPolicy(
75 google_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID));
76 EXPECT_EQ(
77 net::CertPolicy::UNKNOWN,
78 state->QueryPolicy(
79 example_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID));
80
81 // Simulate a user decision to allow an invalid certificate exception for
82 // www_google_gurl.
83 state->AllowCert(
84 www_google_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID);
85
86 // Verify that only www_google_gurl is allowed and that the other two certs
87 // being tested still have no decision associated with them.
88 EXPECT_EQ(
89 net::CertPolicy::ALLOWED,
90 state->QueryPolicy(
91 www_google_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID));
92 EXPECT_EQ(net::CertPolicy::UNKNOWN,
93 state->QueryPolicy(
94 google_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID));
95 EXPECT_EQ(
96 net::CertPolicy::UNKNOWN,
97 state->QueryPolicy(
98 example_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID));
99
100 // Simulate a user decision to allow an invalid certificate exception for
101 // example_gurl.
102 state->AllowCert(
103 example_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID);
104
105 // Verify that both www_google_gurl and example_gurl have allow exceptions
106 // while google_gurl still has no associated decision.
107 EXPECT_EQ(
108 net::CertPolicy::ALLOWED,
109 state->QueryPolicy(
110 www_google_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID));
111 EXPECT_EQ(net::CertPolicy::UNKNOWN,
112 state->QueryPolicy(
113 google_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID));
114 EXPECT_EQ(
115 net::CertPolicy::ALLOWED,
116 state->QueryPolicy(
117 example_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID));
118
119 // Simulate a user decision to deny an invalid certificate for example_gurl.
120 state->DenyCert(
121 example_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID);
122
123 // Verify that www_google_gurl is allowed and example_gurl is denied while
124 // google_gurl still has no associated decision.
125 EXPECT_EQ(
126 net::CertPolicy::ALLOWED,
127 state->QueryPolicy(
128 www_google_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID));
129 EXPECT_EQ(net::CertPolicy::UNKNOWN,
130 state->QueryPolicy(
131 google_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID));
132 EXPECT_EQ(
133 net::CertPolicy::DENIED,
134 state->QueryPolicy(
135 example_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID));
136 }
137
138 // HasPolicyAndRevoke unit tests the expected behavior of calling
139 // HasAllowedOrDeniedCert before and after calling RevokeAllowAndDenyPreferences
140 // on the SSLHostStateDecisions class.
141 IN_PROC_BROWSER_TEST_F(ChromeSSLHostStateDecisionsTest, HasPolicyAndRevoke) {
142 GURL www_google_gurl(kWWWGoogleURL);
143 GURL google_gurl(kGoogleURL);
144 GURL example_gurl(kExampleURL);
145 scoped_refptr<net::X509Certificate> google_cert = GetGoogleCert();
146 content::WebContents* tab =
147 browser()->tab_strip_model()->GetActiveWebContents();
148 Profile* profile = Profile::FromBrowserContext(tab->GetBrowserContext());
149 content::SSLHostStateDecisions* state = profile->GetSSLHostStateDecisions();
150
151 // Simulate a user decision to allow an invalid certificate exception for
152 // www_google_gurl and for example_gurl.
153 state->AllowCert(
154 www_google_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID);
155 state->AllowCert(
156 example_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID);
157
158 // Verify that HasAllowedOrDeniedCert correctly acknowledges that a user
159 // decision has been made about www_google_gurl. Then verify that
160 // HasAllowedOrDeniedCert correctly identifies that the decision has been
161 // revoked.
162 EXPECT_TRUE(state->HasAllowedOrDeniedCert(www_google_gurl));
163 state->RevokeAllowAndDenyPreferences(www_google_gurl);
164 EXPECT_FALSE(state->HasAllowedOrDeniedCert(www_google_gurl));
165 EXPECT_EQ(
166 net::CertPolicy::UNKNOWN,
167 state->QueryPolicy(
168 www_google_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID));
169
170 // Verify that the revocation of the www_google_gurl decision does not affect
171 // the Allow for example_gurl.
172 EXPECT_TRUE(state->HasAllowedOrDeniedCert(example_gurl));
173
174 // Verify the revocation of the www_google_gurl decision does not affect the
175 // non-decision for google_gurl. Then verify that a revocation of a URL with
176 // no decision has no effect.
177 EXPECT_FALSE(state->HasAllowedOrDeniedCert(google_gurl));
178 state->RevokeAllowAndDenyPreferences(google_gurl);
179 EXPECT_FALSE(state->HasAllowedOrDeniedCert(google_gurl));
180 }
181
182 // Clear unit tests the expected behavior of calling Clear to forget all cert
183 // decision state on the SSLHostStateDecisions class.
184 IN_PROC_BROWSER_TEST_F(ChromeSSLHostStateDecisionsTest, Clear) {
185 GURL www_google_gurl(kWWWGoogleURL);
186 GURL example_gurl(kExampleURL);
187 scoped_refptr<net::X509Certificate> google_cert = GetGoogleCert();
188 content::WebContents* tab =
189 browser()->tab_strip_model()->GetActiveWebContents();
190 Profile* profile = Profile::FromBrowserContext(tab->GetBrowserContext());
191 content::SSLHostStateDecisions* state = profile->GetSSLHostStateDecisions();
192
193 // Simulate a user decision to allow an invalid certificate exception for
194 // www_google_gurl and for example_gurl.
195 state->AllowCert(
196 www_google_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID);
197
198 // Do a full clear, then make sure that both www_google_gurl, which had a
199 // decision made, and example_gurl, which was untouched, are now in a
200 // non-decision state.
201 state->Clear();
202 EXPECT_FALSE(state->HasAllowedOrDeniedCert(www_google_gurl));
203 EXPECT_EQ(
204 net::CertPolicy::UNKNOWN,
205 state->QueryPolicy(
206 www_google_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID));
207 EXPECT_FALSE(state->HasAllowedOrDeniedCert(example_gurl));
208 EXPECT_EQ(
209 net::CertPolicy::UNKNOWN,
210 state->QueryPolicy(
211 example_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID));
212 }
213
214 // Tests the basic behavior of cert memory in incognito.
215 class IncognitoSSLHostStateDecisionsTest
216 : public ChromeSSLHostStateDecisionsTest {
217 protected:
218 virtual void SetUpCommandLine(CommandLine* command_line) OVERRIDE {
219 ChromeSSLHostStateDecisionsTest::SetUpCommandLine(command_line);
220 command_line->AppendSwitchASCII(switches::kRememberCertErrorDecisions,
221 kDeltaSecondsString);
222 }
223 };
224
225 IN_PROC_BROWSER_TEST_F(IncognitoSSLHostStateDecisionsTest, PRE_AfterRestart) {
226 GURL www_google_gurl(kWWWGoogleURL);
227 GURL google_gurl(kGoogleURL);
228 scoped_refptr<net::X509Certificate> google_cert = GetGoogleCert();
229 content::WebContents* tab =
230 browser()->tab_strip_model()->GetActiveWebContents();
231 Profile* profile = Profile::FromBrowserContext(tab->GetBrowserContext());
232 content::SSLHostStateDecisions* state = profile->GetSSLHostStateDecisions();
233
234 // Add a cert exception to the profile and then verify that it still exists
235 // in the incognito profile.
236 state->AllowCert(
237 www_google_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID);
238
239 scoped_ptr<Profile> incognito(profile->CreateOffTheRecordProfile());
240 content::SSLHostStateDecisions* incognito_state =
241 incognito->GetSSLHostStateDecisions();
242
243 EXPECT_EQ(
244 net::CertPolicy::ALLOWED,
245 incognito_state->QueryPolicy(
246 www_google_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID));
247
248 // Add a cert exception to the incognito profile. It will be checked after
249 // restart that this exception does not exist. Note the different cert URL and
250 // error than above thus mapping to a second exception. Also validate that it
251 // was not added as an exception to the regular profile.
252 incognito_state->AllowCert(
253 google_gurl, google_cert.get(), net::CERT_STATUS_COMMON_NAME_INVALID);
254
255 EXPECT_EQ(net::CertPolicy::UNKNOWN,
256 state->QueryPolicy(google_gurl,
257 google_cert.get(),
258 net::CERT_STATUS_COMMON_NAME_INVALID));
259 }
260
261 // AfterRestart ensures that any cert decisions made in an incognito profile are
262 // forgetten after a session restart even if given a command line flag to
263 // remember cert decisions after restart.
264 IN_PROC_BROWSER_TEST_F(IncognitoSSLHostStateDecisionsTest, AfterRestart) {
265 GURL www_google_gurl(kWWWGoogleURL);
266 GURL google_gurl(kGoogleURL);
267 scoped_refptr<net::X509Certificate> google_cert = GetGoogleCert();
268 content::WebContents* tab =
269 browser()->tab_strip_model()->GetActiveWebContents();
270 Profile* profile = Profile::FromBrowserContext(tab->GetBrowserContext());
271 content::SSLHostStateDecisions* state = profile->GetSSLHostStateDecisions();
272
273 // Verify that the exception added before restart to the regular
274 // (non-incognito) profile still exists and was not cleared after the
275 // incognito session ended.
276 EXPECT_EQ(
277 net::CertPolicy::ALLOWED,
278 state->QueryPolicy(
279 www_google_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID));
280
281 scoped_ptr<Profile> incognito(profile->CreateOffTheRecordProfile());
282 content::SSLHostStateDecisions* incognito_state =
283 incognito->GetSSLHostStateDecisions();
284
285 // Verify that the exception added before restart to the incognito profile was
286 // cleared when the incognito session ended.
287 EXPECT_EQ(net::CertPolicy::UNKNOWN,
288 incognito_state->QueryPolicy(google_gurl,
289 google_cert.get(),
290 net::CERT_STATUS_COMMON_NAME_INVALID));
291 }
292
293 // Tests to make sure that if the remember value is set to -1, any decisions
294 // won't be remembered over a restart.
295 class ForgetSSLHostStateDecisionsTest : public ChromeSSLHostStateDecisionsTest {
296 protected:
297 virtual void SetUpCommandLine(CommandLine* command_line) OVERRIDE {
298 ChromeSSLHostStateDecisionsTest::SetUpCommandLine(command_line);
299 command_line->AppendSwitchASCII(switches::kRememberCertErrorDecisions,
300 kForgetAtSessionEnd);
301 }
302 };
303
304 IN_PROC_BROWSER_TEST_F(ForgetSSLHostStateDecisionsTest, PRE_AfterRestart) {
305 GURL www_google_gurl(kWWWGoogleURL);
306 scoped_refptr<net::X509Certificate> google_cert = GetGoogleCert();
307 content::WebContents* tab =
308 browser()->tab_strip_model()->GetActiveWebContents();
309 Profile* profile = Profile::FromBrowserContext(tab->GetBrowserContext());
310 content::SSLHostStateDecisions* state = profile->GetSSLHostStateDecisions();
311
312 state->AllowCert(
313 www_google_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID);
314 EXPECT_EQ(
315 net::CertPolicy::ALLOWED,
316 state->QueryPolicy(
317 www_google_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID));
318 }
319
320 IN_PROC_BROWSER_TEST_F(ForgetSSLHostStateDecisionsTest, AfterRestart) {
321 GURL www_google_gurl(kWWWGoogleURL);
322 scoped_refptr<net::X509Certificate> google_cert = GetGoogleCert();
323 content::WebContents* tab =
324 browser()->tab_strip_model()->GetActiveWebContents();
325 Profile* profile = Profile::FromBrowserContext(tab->GetBrowserContext());
326 content::SSLHostStateDecisions* state = profile->GetSSLHostStateDecisions();
327
328 // The cert should now be |UNKONWN| because the profile is set to forget cert
329 // exceptions after session end.
330 EXPECT_EQ(
331 net::CertPolicy::UNKNOWN,
332 state->QueryPolicy(
333 www_google_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID));
334 }
335
336 // Tests to make sure that if the remember value is set to 0, any decisions made
337 // will be forgetten immediately.
338 class ForgetInstantlySSLHostStateDecisionsTest
339 : public ChromeSSLHostStateDecisionsTest {
340 protected:
341 virtual void SetUpCommandLine(CommandLine* command_line) OVERRIDE {
342 ChromeSSLHostStateDecisionsTest::SetUpCommandLine(command_line);
343 command_line->AppendSwitchASCII(switches::kRememberCertErrorDecisions,
344 kForgetInstantly);
345 }
346 };
347
348 IN_PROC_BROWSER_TEST_F(ForgetInstantlySSLHostStateDecisionsTest,
349 MakeAndForgetException) {
350 GURL www_google_gurl(kWWWGoogleURL);
351 scoped_refptr<net::X509Certificate> google_cert = GetGoogleCert();
352 content::WebContents* tab =
353 browser()->tab_strip_model()->GetActiveWebContents();
354 Profile* profile = Profile::FromBrowserContext(tab->GetBrowserContext());
355 content::SSLHostStateDecisions* state = profile->GetSSLHostStateDecisions();
356
357 // chrome_state takes ownership of this clock
358 base::SimpleTestClock* clock = new base::SimpleTestClock();
359 ChromeSSLHostStateDecisions* chrome_state =
360 static_cast<ChromeSSLHostStateDecisions*>(state);
361 chrome_state->SetClock(scoped_ptr<base::Clock>(clock));
362
363 // Start the clock at standard system time but do not advance at all to
364 // emphasize that instant forget works.
365 clock->SetNow(base::Time::NowFromSystemTime());
366
367 state->AllowCert(
368 www_google_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID);
369 EXPECT_EQ(
370 net::CertPolicy::UNKNOWN,
371 state->QueryPolicy(
372 www_google_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID));
373 }
374
375 // Tests to make sure that if the remember value is set to a non-zero value0,
376 // any decisions will be remembered over a restart, but only for the length
377 // specified.
378 class RememberSSLHostStateDecisionsTest
379 : public ChromeSSLHostStateDecisionsTest {
380 protected:
381 virtual void SetUpCommandLine(CommandLine* command_line) OVERRIDE {
382 ChromeSSLHostStateDecisionsTest::SetUpCommandLine(command_line);
383 command_line->AppendSwitchASCII(switches::kRememberCertErrorDecisions,
384 kDeltaSecondsString);
385 }
386 };
387
388 IN_PROC_BROWSER_TEST_F(RememberSSLHostStateDecisionsTest, PRE_AfterRestart) {
389 GURL www_google_gurl(kWWWGoogleURL);
390 scoped_refptr<net::X509Certificate> google_cert = GetGoogleCert();
391 content::WebContents* tab =
392 browser()->tab_strip_model()->GetActiveWebContents();
393 Profile* profile = Profile::FromBrowserContext(tab->GetBrowserContext());
394 content::SSLHostStateDecisions* state = profile->GetSSLHostStateDecisions();
395
396 state->AllowCert(
397 www_google_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID);
398 EXPECT_EQ(
399 net::CertPolicy::ALLOWED,
400 state->QueryPolicy(
401 www_google_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID));
402 }
403
404 IN_PROC_BROWSER_TEST_F(RememberSSLHostStateDecisionsTest, AfterRestart) {
405 GURL www_google_gurl(kWWWGoogleURL);
406 scoped_refptr<net::X509Certificate> google_cert = GetGoogleCert();
407 content::WebContents* tab =
408 browser()->tab_strip_model()->GetActiveWebContents();
409 Profile* profile = Profile::FromBrowserContext(tab->GetBrowserContext());
410 content::SSLHostStateDecisions* state = profile->GetSSLHostStateDecisions();
411
412 // chrome_state takes ownership of this clock
413 base::SimpleTestClock* clock = new base::SimpleTestClock();
414 ChromeSSLHostStateDecisions* chrome_state =
415 static_cast<ChromeSSLHostStateDecisions*>(state);
416 chrome_state->SetClock(scoped_ptr<base::Clock>(clock));
417
418 // Start the clock at standard system time.
419 clock->SetNow(base::Time::NowFromSystemTime());
420
421 // This should only pass if the cert was allowed before the test was restart
422 // and thus has now been rememebered across browser restarts.
423 EXPECT_EQ(
424 net::CertPolicy::ALLOWED,
425 state->QueryPolicy(
426 www_google_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID));
427
428 // Simulate the clock advancing by the specified delta.
429 clock->Advance(base::TimeDelta::FromSeconds(kDeltaOneDayInSeconds + 1));
430
431 // The cert should now be |UNKONWN| because the specified delta has passed.
432 EXPECT_EQ(
433 net::CertPolicy::UNKNOWN,
434 state->QueryPolicy(
435 www_google_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID));
436 }
437
438 // Tests to make sure that if the user deletes their browser history, SSL
439 // exceptions will be deleted as well.
440 class RemoveBrowsingHistorySSLHostStateDecisionsTest
441 : public ChromeSSLHostStateDecisionsTest {
442 public:
443 void RemoveAndWait(Profile* profile) {
444 BrowsingDataRemover* remover = BrowsingDataRemover::CreateForPeriod(
445 profile, BrowsingDataRemover::LAST_HOUR);
446 BrowsingDataRemoverCompletionObserver completion_observer(remover);
447 remover->Remove(BrowsingDataRemover::REMOVE_HISTORY,
448 BrowsingDataHelper::UNPROTECTED_WEB);
449 completion_observer.BlockUntilCompletion();
450 }
451 };
452
453 IN_PROC_BROWSER_TEST_F(RemoveBrowsingHistorySSLHostStateDecisionsTest,
454 DeleteHistory) {
455 GURL google_gurl(kGoogleURL);
456 scoped_refptr<net::X509Certificate> google_cert = GetGoogleCert();
457 content::WebContents* tab =
458 browser()->tab_strip_model()->GetActiveWebContents();
459 Profile* profile = Profile::FromBrowserContext(tab->GetBrowserContext());
460 content::SSLHostStateDecisions* state = profile->GetSSLHostStateDecisions();
461
462 // Add an exception for an invalid certificate. Then remove the last hour's
463 // worth of browsing history and verify that the exception has been deleted.
464 state->AllowCert(
465 google_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID);
466 RemoveAndWait(profile);
467 EXPECT_EQ(net::CertPolicy::UNKNOWN,
468 state->QueryPolicy(
469 google_gurl, google_cert.get(), net::CERT_STATUS_DATE_INVALID));
470 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698