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

Side by Side Diff: chrome/browser/net/sqlite_origin_bound_cert_store_unittest.cc

Issue 8890073: Handle Origin Bound Certificate expiration. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Created 9 years 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
1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "base/bind.h" 5 #include "base/bind.h"
6 #include "base/file_util.h" 6 #include "base/file_util.h"
7 #include "base/memory/ref_counted.h" 7 #include "base/memory/ref_counted.h"
8 #include "base/message_loop.h" 8 #include "base/message_loop.h"
9 #include "base/scoped_temp_dir.h" 9 #include "base/scoped_temp_dir.h"
10 #include "base/stl_util.h" 10 #include "base/stl_util.h"
11 #include "base/test/thread_test_helper.h" 11 #include "base/test/thread_test_helper.h"
12 #include "chrome/browser/net/sqlite_origin_bound_cert_store.h" 12 #include "chrome/browser/net/sqlite_origin_bound_cert_store.h"
13 #include "chrome/common/chrome_constants.h" 13 #include "chrome/common/chrome_constants.h"
14 #include "content/test/test_browser_thread.h" 14 #include "content/test/test_browser_thread.h"
15 #include "net/base/cert_test_util.h"
15 #include "sql/statement.h" 16 #include "sql/statement.h"
16 #include "testing/gtest/include/gtest/gtest.h" 17 #include "testing/gtest/include/gtest/gtest.h"
17 18
18 using content::BrowserThread; 19 using content::BrowserThread;
19 20
20 class SQLiteOriginBoundCertStoreTest : public testing::Test { 21 class SQLiteOriginBoundCertStoreTest : public testing::Test {
21 public: 22 public:
22 SQLiteOriginBoundCertStoreTest() 23 SQLiteOriginBoundCertStoreTest()
23 : db_thread_(BrowserThread::DB) { 24 : db_thread_(BrowserThread::DB) {
24 } 25 }
25 26
26 protected: 27 protected:
28 static void ReadTestKeyAndCert(std::string* key, std::string* cert) {
29 FilePath key_path = net::GetTestCertsDirectory().AppendASCII(
30 "unittest.originbound.key.der");
31 FilePath cert_path = net::GetTestCertsDirectory().AppendASCII(
32 "unittest.originbound.der");
33 ASSERT_TRUE(file_util::ReadFileToString(key_path, key));
34 ASSERT_TRUE(file_util::ReadFileToString(cert_path, cert));
35 }
36
37 static base::Time GetTestCertExpirationTime() {
38 // Cert expiration time from 'dumpasn1 unittest.originbound.der':
39 // GeneralizedTime 19/11/2111 02:23:45 GMT
40 base::Time::Exploded exploded_time;
41 exploded_time.year = 2111;
42 exploded_time.month = 11;
43 exploded_time.day_of_week = 0; // Unused.
44 exploded_time.day_of_month = 19;
45 exploded_time.hour = 2;
46 exploded_time.minute = 23;
47 exploded_time.second = 45;
48 exploded_time.millisecond = 0;
49 return base::Time::FromUTCExploded(exploded_time);
50 }
51
27 virtual void SetUp() { 52 virtual void SetUp() {
28 db_thread_.Start(); 53 db_thread_.Start();
29 ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); 54 ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
30 store_ = new SQLiteOriginBoundCertStore( 55 store_ = new SQLiteOriginBoundCertStore(
31 temp_dir_.path().Append(chrome::kOBCertFilename)); 56 temp_dir_.path().Append(chrome::kOBCertFilename));
32 std::vector<net::DefaultOriginBoundCertStore::OriginBoundCert*> certs; 57 std::vector<net::DefaultOriginBoundCertStore::OriginBoundCert*> certs;
33 ASSERT_TRUE(store_->Load(&certs)); 58 ASSERT_TRUE(store_->Load(&certs));
34 ASSERT_EQ(0u, certs.size()); 59 ASSERT_EQ(0u, certs.size());
35 // Make sure the store gets written at least once. 60 // Make sure the store gets written at least once.
36 store_->AddOriginBoundCert( 61 store_->AddOriginBoundCert(
37 net::DefaultOriginBoundCertStore::OriginBoundCert( 62 net::DefaultOriginBoundCertStore::OriginBoundCert(
38 "https://encrypted.google.com:8443", 63 "https://encrypted.google.com:8443",
39 net::CLIENT_CERT_RSA_SIGN, "a", "b")); 64 net::CLIENT_CERT_RSA_SIGN,
65 base::Time(),
66 "a", "b"));
40 } 67 }
41 68
42 content::TestBrowserThread db_thread_; 69 content::TestBrowserThread db_thread_;
43 ScopedTempDir temp_dir_; 70 ScopedTempDir temp_dir_;
44 scoped_refptr<SQLiteOriginBoundCertStore> store_; 71 scoped_refptr<SQLiteOriginBoundCertStore> store_;
45 }; 72 };
46 73
47 TEST_F(SQLiteOriginBoundCertStoreTest, KeepOnDestruction) { 74 TEST_F(SQLiteOriginBoundCertStoreTest, KeepOnDestruction) {
48 store_->SetClearLocalStateOnExit(false); 75 store_->SetClearLocalStateOnExit(false);
49 store_ = NULL; 76 store_ = NULL;
(...skipping 22 matching lines...) Expand all
72 ASSERT_TRUE(helper->Run()); 99 ASSERT_TRUE(helper->Run());
73 100
74 ASSERT_FALSE(file_util::PathExists( 101 ASSERT_FALSE(file_util::PathExists(
75 temp_dir_.path().Append(chrome::kOBCertFilename))); 102 temp_dir_.path().Append(chrome::kOBCertFilename)));
76 } 103 }
77 104
78 // Test if data is stored as expected in the SQLite database. 105 // Test if data is stored as expected in the SQLite database.
79 TEST_F(SQLiteOriginBoundCertStoreTest, TestPersistence) { 106 TEST_F(SQLiteOriginBoundCertStoreTest, TestPersistence) {
80 store_->AddOriginBoundCert( 107 store_->AddOriginBoundCert(
81 net::DefaultOriginBoundCertStore::OriginBoundCert( 108 net::DefaultOriginBoundCertStore::OriginBoundCert(
82 "https://www.google.com/", net::CLIENT_CERT_ECDSA_SIGN, "c", "d")); 109 "https://www.google.com/",
110 net::CLIENT_CERT_ECDSA_SIGN,
111 base::Time(),
112 "c", "d"));
83 113
84 std::vector<net::DefaultOriginBoundCertStore::OriginBoundCert*> certs; 114 std::vector<net::DefaultOriginBoundCertStore::OriginBoundCert*> certs;
85 // Replace the store effectively destroying the current one and forcing it 115 // Replace the store effectively destroying the current one and forcing it
86 // to write it's data to disk. Then we can see if after loading it again it 116 // to write it's data to disk. Then we can see if after loading it again it
87 // is still there. 117 // is still there.
88 store_ = NULL; 118 store_ = NULL;
89 scoped_refptr<base::ThreadTestHelper> helper( 119 scoped_refptr<base::ThreadTestHelper> helper(
90 new base::ThreadTestHelper( 120 new base::ThreadTestHelper(
91 BrowserThread::GetMessageLoopProxyForThread(BrowserThread::DB))); 121 BrowserThread::GetMessageLoopProxyForThread(BrowserThread::DB)));
92 // Make sure we wait until the destructor has run. 122 // Make sure we wait until the destructor has run.
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after
124 STLDeleteContainerPointers(certs.begin(), certs.end()); 154 STLDeleteContainerPointers(certs.begin(), certs.end());
125 certs.clear(); 155 certs.clear();
126 store_ = new SQLiteOriginBoundCertStore( 156 store_ = new SQLiteOriginBoundCertStore(
127 temp_dir_.path().Append(chrome::kOBCertFilename)); 157 temp_dir_.path().Append(chrome::kOBCertFilename));
128 158
129 // Reload and check if the cert has been removed. 159 // Reload and check if the cert has been removed.
130 ASSERT_TRUE(store_->Load(&certs)); 160 ASSERT_TRUE(store_->Load(&certs));
131 ASSERT_EQ(0U, certs.size()); 161 ASSERT_EQ(0U, certs.size());
132 } 162 }
133 163
134 TEST_F(SQLiteOriginBoundCertStoreTest, TestUpgrade) { 164 TEST_F(SQLiteOriginBoundCertStoreTest, TestUpgrade) {
wtc 2011/12/15 23:34:28 Should we rename this test TestUpgradeV1?
mattm 2011/12/20 00:28:38 Done.
135 // Reset the store. We'll be using a different database for this test. 165 // Reset the store. We'll be using a different database for this test.
136 store_ = NULL; 166 store_ = NULL;
137 167
138 FilePath v1_db_path(temp_dir_.path().AppendASCII("v1db")); 168 FilePath v1_db_path(temp_dir_.path().AppendASCII("v1db"));
139 169
170 std::string key_data;
171 std::string cert_data;
172 ReadTestKeyAndCert(&key_data, &cert_data);
173
140 // Create a version 1 database. 174 // Create a version 1 database.
141 { 175 {
142 sql::Connection db; 176 sql::Connection db;
143 ASSERT_TRUE(db.Open(v1_db_path)); 177 ASSERT_TRUE(db.Open(v1_db_path));
144 ASSERT_TRUE(db.Execute( 178 ASSERT_TRUE(db.Execute(
145 "CREATE TABLE meta(key LONGVARCHAR NOT NULL UNIQUE PRIMARY KEY," 179 "CREATE TABLE meta(key LONGVARCHAR NOT NULL UNIQUE PRIMARY KEY,"
146 "value LONGVARCHAR);" 180 "value LONGVARCHAR);"
147 "INSERT INTO \"meta\" VALUES('version','1');" 181 "INSERT INTO \"meta\" VALUES('version','1');"
148 "INSERT INTO \"meta\" VALUES('last_compatible_version','1');" 182 "INSERT INTO \"meta\" VALUES('last_compatible_version','1');"
149 "CREATE TABLE origin_bound_certs (" 183 "CREATE TABLE origin_bound_certs ("
150 "origin TEXT NOT NULL UNIQUE PRIMARY KEY," 184 "origin TEXT NOT NULL UNIQUE PRIMARY KEY,"
151 "private_key BLOB NOT NULL,cert BLOB NOT NULL);" 185 "private_key BLOB NOT NULL,cert BLOB NOT NULL);"));
152 "INSERT INTO \"origin_bound_certs\" VALUES(" 186
153 "'https://google.com',X'AA',X'BB');" 187 sql::Statement add_smt(db.GetUniqueStatement(
188 "INSERT INTO origin_bound_certs (origin, private_key, cert) "
189 "VALUES (?,?,?)"));
190 add_smt.BindString(0, "https://www.google.com:443");
wtc 2011/12/15 23:34:28 Nit: to be more realistic, should we omit the defa
mattm 2011/12/20 00:28:38 That's actually how the origin is stored by the co
191 add_smt.BindBlob(1, key_data.data(), key_data.size());
192 add_smt.BindBlob(2, cert_data.data(), cert_data.size());
193 ASSERT_TRUE(add_smt.Run());
194
195 ASSERT_TRUE(db.Execute(
154 "INSERT INTO \"origin_bound_certs\" VALUES(" 196 "INSERT INTO \"origin_bound_certs\" VALUES("
155 "'https://foo.com',X'CC',X'DD');" 197 "'https://foo.com',X'CC',X'DD');"
wtc 2011/12/15 23:34:28 Nit: this cert can use X'AA' and X'BB' now. This
mattm 2011/12/20 00:28:38 Done.
156 )); 198 ));
157 } 199 }
158 200
159 std::vector<net::DefaultOriginBoundCertStore::OriginBoundCert*> certs; 201 // Load and test the DB contents twice. First time ensures that we can use
160 store_ = new SQLiteOriginBoundCertStore(v1_db_path); 202 // the updated values immediately. Second time ensures that the updated
203 // values are stored and read correctly on next load.
204 for (int i = 0; i < 2; ++i) {
205 SCOPED_TRACE(i);
161 206
162 // Load the database and ensure the certs can be read and are marked as RSA. 207 std::vector<net::DefaultOriginBoundCertStore::OriginBoundCert*> certs;
163 ASSERT_TRUE(store_->Load(&certs)); 208 store_ = new SQLiteOriginBoundCertStore(v1_db_path);
164 ASSERT_EQ(2U, certs.size());
165 ASSERT_STREQ("https://google.com", certs[0]->origin().c_str());
166 ASSERT_EQ(net::CLIENT_CERT_RSA_SIGN, certs[0]->type());
167 ASSERT_STREQ("\xaa", certs[0]->private_key().c_str());
168 ASSERT_STREQ("\xbb", certs[0]->cert().c_str());
169 ASSERT_STREQ("https://foo.com", certs[1]->origin().c_str());
170 ASSERT_EQ(net::CLIENT_CERT_RSA_SIGN, certs[1]->type());
171 ASSERT_STREQ("\xcc", certs[1]->private_key().c_str());
172 ASSERT_STREQ("\xdd", certs[1]->cert().c_str());
173 209
174 STLDeleteContainerPointers(certs.begin(), certs.end()); 210 // Load the database and ensure the certs can be read and are marked as RSA.
175 certs.clear(); 211 ASSERT_TRUE(store_->Load(&certs));
212 ASSERT_EQ(2U, certs.size());
176 213
177 store_ = NULL; 214 ASSERT_STREQ("https://www.google.com:443", certs[0]->origin().c_str());
178 // Make sure we wait until the destructor has run. 215 ASSERT_EQ(net::CLIENT_CERT_RSA_SIGN, certs[0]->type());
179 scoped_refptr<base::ThreadTestHelper> helper( 216 ASSERT_EQ(GetTestCertExpirationTime(),
180 new base::ThreadTestHelper( 217 certs[0]->not_valid_after());
181 BrowserThread::GetMessageLoopProxyForThread(BrowserThread::DB))); 218 ASSERT_EQ(key_data, certs[0]->private_key());
182 ASSERT_TRUE(helper->Run()); 219 ASSERT_EQ(cert_data, certs[0]->cert());
183 220
184 // Verify the database version is updated. 221 ASSERT_STREQ("https://foo.com", certs[1]->origin().c_str());
185 { 222 ASSERT_EQ(net::CLIENT_CERT_RSA_SIGN, certs[1]->type());
186 sql::Connection db; 223 // Undecodable cert, expiration time will be uninitialized.
187 ASSERT_TRUE(db.Open(v1_db_path)); 224 ASSERT_EQ(base::Time(), certs[1]->not_valid_after());
188 sql::Statement smt(db.GetUniqueStatement( 225 ASSERT_STREQ("\xcc", certs[1]->private_key().c_str());
189 "SELECT value FROM meta WHERE key = \"version\"")); 226 ASSERT_STREQ("\xdd", certs[1]->cert().c_str());
190 ASSERT_TRUE(smt); 227
191 ASSERT_TRUE(smt.Step()); 228 STLDeleteContainerPointers(certs.begin(), certs.end());
192 EXPECT_EQ(2, smt.ColumnInt(0)); 229 certs.clear();
wtc 2011/12/15 23:34:28 I believe these two lines are equivalent to: S
mattm 2011/12/20 00:28:38 Done.
193 EXPECT_FALSE(smt.Step()); 230
231 store_ = NULL;
232 // Make sure we wait until the destructor has run.
233 scoped_refptr<base::ThreadTestHelper> helper(
234 new base::ThreadTestHelper(
235 BrowserThread::GetMessageLoopProxyForThread(BrowserThread::DB)));
236 ASSERT_TRUE(helper->Run());
237
238 // Verify the database version is updated.
239 {
240 sql::Connection db;
241 ASSERT_TRUE(db.Open(v1_db_path));
242 sql::Statement smt(db.GetUniqueStatement(
243 "SELECT value FROM meta WHERE key = \"version\""));
244 ASSERT_TRUE(smt);
245 ASSERT_TRUE(smt.Step());
246 EXPECT_EQ(3, smt.ColumnInt(0));
247 EXPECT_FALSE(smt.Step());
248 }
194 } 249 }
195 } 250 }
196 251
252 TEST_F(SQLiteOriginBoundCertStoreTest, TestUpgradeV2) {
253 // Reset the store. We'll be using a different database for this test.
254 store_ = NULL;
255
256 FilePath v2_db_path(temp_dir_.path().AppendASCII("v2db"));
257
258 std::string key_data;
259 std::string cert_data;
260 ReadTestKeyAndCert(&key_data, &cert_data);
261
262 // Create a version 2 database.
263 {
264 sql::Connection db;
265 ASSERT_TRUE(db.Open(v2_db_path));
266 ASSERT_TRUE(db.Execute(
267 "CREATE TABLE meta(key LONGVARCHAR NOT NULL UNIQUE PRIMARY KEY,"
268 "value LONGVARCHAR);"
269 "INSERT INTO \"meta\" VALUES('version','2');"
270 "INSERT INTO \"meta\" VALUES('last_compatible_version','1');"
271 "CREATE TABLE origin_bound_certs ("
272 "origin TEXT NOT NULL UNIQUE PRIMARY KEY,"
273 "private_key BLOB NOT NULL,"
274 "cert BLOB NOT NULL,"
275 "cert_type INTEGER);"
276 ));
277
278 sql::Statement add_smt(db.GetUniqueStatement(
279 "INSERT INTO origin_bound_certs (origin, private_key, cert, cert_type) "
280 "VALUES (?,?,?,?)"));
281 add_smt.BindString(0, "https://www.google.com:443");
282 add_smt.BindBlob(1, key_data.data(), key_data.size());
283 add_smt.BindBlob(2, cert_data.data(), cert_data.size());
284 add_smt.BindInt64(3, 1);
285 ASSERT_TRUE(add_smt.Run());
286
287 ASSERT_TRUE(db.Execute(
288 "INSERT INTO \"origin_bound_certs\" VALUES("
289 "'https://foo.com',X'CC',X'DD',64);"
290 ));
291 }
292
293
wtc 2011/12/15 23:34:28 Nit: delete one blank line.
mattm 2011/12/20 00:28:38 Done.
294 // Load and test the DB contents twice. First time ensures that we can use
295 // the updated values immediately. Second time ensures that the updated
296 // values are saved and read correctly on next load.
297 for (int i = 0; i < 2; ++i) {
298 SCOPED_TRACE(i);
299
300 std::vector<net::DefaultOriginBoundCertStore::OriginBoundCert*> certs;
301 store_ = new SQLiteOriginBoundCertStore(v2_db_path);
302
303 // Load the database and ensure the certs can be read and are marked as RSA.
304 ASSERT_TRUE(store_->Load(&certs));
305 ASSERT_EQ(2U, certs.size());
306
307 ASSERT_STREQ("https://www.google.com:443", certs[0]->origin().c_str());
308 ASSERT_EQ(net::CLIENT_CERT_RSA_SIGN, certs[0]->type());
309 ASSERT_EQ(GetTestCertExpirationTime(),
310 certs[0]->not_valid_after());
311 ASSERT_EQ(key_data, certs[0]->private_key());
312 ASSERT_EQ(cert_data, certs[0]->cert());
313
314 ASSERT_STREQ("https://foo.com", certs[1]->origin().c_str());
315 ASSERT_EQ(net::CLIENT_CERT_ECDSA_SIGN, certs[1]->type());
316 // Undecodable cert, expiration time will be uninitialized.
317 ASSERT_EQ(base::Time(), certs[1]->not_valid_after());
318 ASSERT_STREQ("\xcc", certs[1]->private_key().c_str());
319 ASSERT_STREQ("\xdd", certs[1]->cert().c_str());
320
321 STLDeleteContainerPointers(certs.begin(), certs.end());
322 certs.clear();
323
324 store_ = NULL;
325 // Make sure we wait until the destructor has run.
326 scoped_refptr<base::ThreadTestHelper> helper(
327 new base::ThreadTestHelper(
328 BrowserThread::GetMessageLoopProxyForThread(BrowserThread::DB)));
329 ASSERT_TRUE(helper->Run());
330
331 // Verify the database version is updated.
332 {
333 sql::Connection db;
334 ASSERT_TRUE(db.Open(v2_db_path));
335 sql::Statement smt(db.GetUniqueStatement(
336 "SELECT value FROM meta WHERE key = \"version\""));
337 ASSERT_TRUE(smt);
338 ASSERT_TRUE(smt.Step());
339 EXPECT_EQ(3, smt.ColumnInt(0));
340 EXPECT_FALSE(smt.Step());
341 }
342 }
343 }
344
197 // Test that we can force the database to be written by calling Flush(). 345 // Test that we can force the database to be written by calling Flush().
198 TEST_F(SQLiteOriginBoundCertStoreTest, TestFlush) { 346 TEST_F(SQLiteOriginBoundCertStoreTest, TestFlush) {
199 // File timestamps don't work well on all platforms, so we'll determine 347 // File timestamps don't work well on all platforms, so we'll determine
200 // whether the DB file has been modified by checking its size. 348 // whether the DB file has been modified by checking its size.
201 FilePath path = temp_dir_.path().Append(chrome::kOBCertFilename); 349 FilePath path = temp_dir_.path().Append(chrome::kOBCertFilename);
202 base::PlatformFileInfo info; 350 base::PlatformFileInfo info;
203 ASSERT_TRUE(file_util::GetFileInfo(path, &info)); 351 ASSERT_TRUE(file_util::GetFileInfo(path, &info));
204 int64 base_size = info.size; 352 int64 base_size = info.size;
205 353
206 // Write some certs, so the DB will have to expand by several KB. 354 // Write some certs, so the DB will have to expand by several KB.
207 for (char c = 'a'; c < 'z'; ++c) { 355 for (char c = 'a'; c < 'z'; ++c) {
208 std::string origin(1, c); 356 std::string origin(1, c);
209 std::string private_key(1000, c); 357 std::string private_key(1000, c);
210 std::string cert(1000, c); 358 std::string cert(1000, c);
211 store_->AddOriginBoundCert( 359 store_->AddOriginBoundCert(
212 net::DefaultOriginBoundCertStore::OriginBoundCert( 360 net::DefaultOriginBoundCertStore::OriginBoundCert(
213 origin, 361 origin,
214 net::CLIENT_CERT_RSA_SIGN, 362 net::CLIENT_CERT_RSA_SIGN,
363 base::Time(),
215 private_key, 364 private_key,
216 cert)); 365 cert));
217 } 366 }
218 367
219 // Call Flush() and wait until the DB thread is idle. 368 // Call Flush() and wait until the DB thread is idle.
220 store_->Flush(base::Closure()); 369 store_->Flush(base::Closure());
221 scoped_refptr<base::ThreadTestHelper> helper( 370 scoped_refptr<base::ThreadTestHelper> helper(
222 new base::ThreadTestHelper( 371 new base::ThreadTestHelper(
223 BrowserThread::GetMessageLoopProxyForThread(BrowserThread::DB))); 372 BrowserThread::GetMessageLoopProxyForThread(BrowserThread::DB)));
224 ASSERT_TRUE(helper->Run()); 373 ASSERT_TRUE(helper->Run());
(...skipping 30 matching lines...) Expand all
255 404
256 store_->Flush(base::Bind(&CallbackCounter::Callback, counter.get())); 405 store_->Flush(base::Bind(&CallbackCounter::Callback, counter.get()));
257 406
258 scoped_refptr<base::ThreadTestHelper> helper( 407 scoped_refptr<base::ThreadTestHelper> helper(
259 new base::ThreadTestHelper( 408 new base::ThreadTestHelper(
260 BrowserThread::GetMessageLoopProxyForThread(BrowserThread::DB))); 409 BrowserThread::GetMessageLoopProxyForThread(BrowserThread::DB)));
261 ASSERT_TRUE(helper->Run()); 410 ASSERT_TRUE(helper->Run());
262 411
263 ASSERT_EQ(1, counter->callback_count()); 412 ASSERT_EQ(1, counter->callback_count());
264 } 413 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698