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

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

Issue 48091: SSLPolicy Fix: Step 8.... (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src/
Patch Set: '' Created 11 years, 9 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
« no previous file with comments | « chrome/browser/ssl/ssl_policy.h ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2006-2008 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 "chrome/browser/ssl/ssl_policy.h" 5 #include "chrome/browser/ssl/ssl_policy.h"
6 6
7 #include "base/singleton.h" 7 #include "base/singleton.h"
8 #include "base/string_piece.h" 8 #include "base/string_piece.h"
9 #include "base/string_util.h" 9 #include "base/string_util.h"
10 #include "chrome/browser/cert_store.h" 10 #include "chrome/browser/cert_store.h"
(...skipping 20 matching lines...) Expand all
31 #if defined(OS_WIN) 31 #if defined(OS_WIN)
32 // TODO(port): port these files. 32 // TODO(port): port these files.
33 #include "chrome/browser/tab_contents/tab_contents.h" 33 #include "chrome/browser/tab_contents/tab_contents.h"
34 #elif defined(OS_POSIX) 34 #elif defined(OS_POSIX)
35 #include "chrome/common/temp_scaffolding_stubs.h" 35 #include "chrome/common/temp_scaffolding_stubs.h"
36 #endif 36 #endif
37 37
38 // Wrap all these helper classes in an anonymous namespace. 38 // Wrap all these helper classes in an anonymous namespace.
39 namespace { 39 namespace {
40 40
41 class ShowUnsafeContentTask : public Task { 41 class ShowMixedContentTask : public Task {
42 public: 42 public:
43 ShowUnsafeContentTask(const GURL& main_frame_url, 43 ShowMixedContentTask(SSLManager::MixedContentHandler* handler);
44 SSLManager::ErrorHandler* error_handler); 44 virtual ~ShowMixedContentTask();
45 virtual ~ShowUnsafeContentTask();
46 45
47 virtual void Run(); 46 virtual void Run();
48 47
49 private: 48 private:
50 scoped_refptr<SSLManager::ErrorHandler> error_handler_; 49 scoped_refptr<SSLManager::MixedContentHandler> handler_;
51 GURL main_frame_url_;
52 50
53 DISALLOW_EVIL_CONSTRUCTORS(ShowUnsafeContentTask); 51 DISALLOW_COPY_AND_ASSIGN(ShowMixedContentTask);
54 }; 52 };
55 53
56 ShowUnsafeContentTask::ShowUnsafeContentTask( 54 ShowMixedContentTask::ShowMixedContentTask(
57 const GURL& main_frame_url, 55 SSLManager::MixedContentHandler* handler)
58 SSLManager::ErrorHandler* error_handler) 56 : handler_(handler) {
59 : error_handler_(error_handler),
60 main_frame_url_(main_frame_url) {
61 } 57 }
62 58
63 ShowUnsafeContentTask::~ShowUnsafeContentTask() { 59 ShowMixedContentTask::~ShowMixedContentTask() {
64 } 60 }
65 61
66 void ShowUnsafeContentTask::Run() { 62 void ShowMixedContentTask::Run() {
67 error_handler_->manager()->AllowShowInsecureContentForURL(main_frame_url_); 63 handler_->manager()->AllowMixedContentForHost(
64 GURL(handler_->main_frame_origin()).host());
65
68 // Reload the page. 66 // Reload the page.
69 error_handler_->GetWebContents()->controller()->Reload(true); 67 handler_->manager()->controller()->Reload(true);
70 } 68 }
71 69
72 static void ShowErrorPage(SSLPolicy* policy, SSLManager::CertError* error) { 70 static void ShowErrorPage(SSLPolicy* policy, SSLManager::CertError* error) {
73 SSLErrorInfo error_info = policy->GetSSLErrorInfo(error); 71 SSLErrorInfo error_info = policy->GetSSLErrorInfo(error);
74 72
75 // Let's build the html error page. 73 // Let's build the html error page.
76 DictionaryValue strings; 74 DictionaryValue strings;
77 strings.SetString(L"title", l10n_util::GetString(IDS_SSL_ERROR_PAGE_TITLE)); 75 strings.SetString(L"title", l10n_util::GetString(IDS_SSL_ERROR_PAGE_TITLE));
78 strings.SetString(L"headLine", error_info.title()); 76 strings.SetString(L"headLine", error_info.title());
79 strings.SetString(L"description", error_info.details()); 77 strings.SetString(L"description", error_info.details());
(...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after
116 114
117 } // namespace 115 } // namespace
118 116
119 SSLPolicy::SSLPolicy() { 117 SSLPolicy::SSLPolicy() {
120 } 118 }
121 119
122 SSLPolicy* SSLPolicy::GetDefaultPolicy() { 120 SSLPolicy* SSLPolicy::GetDefaultPolicy() {
123 return Singleton<SSLPolicy>::get(); 121 return Singleton<SSLPolicy>::get();
124 } 122 }
125 123
126 void SSLPolicy::OnCertError(const GURL& main_frame_url, 124 void SSLPolicy::OnCertError(SSLManager::CertError* error) {
127 SSLManager::CertError* error) {
128 // First we check if we know the policy for this error. 125 // First we check if we know the policy for this error.
129 net::X509Certificate::Policy::Judgment judgment = 126 net::X509Certificate::Policy::Judgment judgment =
130 error->manager()->QueryPolicy(error->ssl_info().cert, 127 error->manager()->QueryPolicy(error->ssl_info().cert,
131 error->request_url().host()); 128 error->request_url().host());
132 129
133 if (judgment == net::X509Certificate::Policy::ALLOWED) { 130 if (judgment == net::X509Certificate::Policy::ALLOWED) {
134 // We've been told to allow this certificate. 131 // We've been told to allow this certificate.
135 if (error->manager()->SetMaxSecurityStyle( 132 if (error->manager()->SetMaxSecurityStyle(
136 SECURITY_STYLE_AUTHENTICATION_BROKEN)) { 133 SECURITY_STYLE_AUTHENTICATION_BROKEN)) {
137 NotificationService::current()->Notify( 134 NotificationService::current()->Notify(
138 NotificationType::SSL_VISIBLE_STATE_CHANGED, 135 NotificationType::SSL_VISIBLE_STATE_CHANGED,
139 Source<NavigationController>(error->manager()->controller()), 136 Source<NavigationController>(error->manager()->controller()),
140 Details<NavigationEntry>( 137 Details<NavigationEntry>(
141 error->manager()->controller()->GetActiveEntry())); 138 error->manager()->controller()->GetActiveEntry()));
142 } 139 }
143 error->ContinueRequest(); 140 error->ContinueRequest();
144 return; 141 return;
145 } 142 }
146 143
147 // The judgment is either DENIED or UNKNOWN. 144 // The judgment is either DENIED or UNKNOWN.
148 // For now we handle the DENIED as the UNKNOWN, which means a blocking 145 // For now we handle the DENIED as the UNKNOWN, which means a blocking
149 // page is shown to the user every time he comes back to the page. 146 // page is shown to the user every time he comes back to the page.
150 147
151 switch(error->cert_error()) { 148 switch(error->cert_error()) {
152 case net::ERR_CERT_COMMON_NAME_INVALID: 149 case net::ERR_CERT_COMMON_NAME_INVALID:
153 case net::ERR_CERT_DATE_INVALID: 150 case net::ERR_CERT_DATE_INVALID:
154 case net::ERR_CERT_AUTHORITY_INVALID: 151 case net::ERR_CERT_AUTHORITY_INVALID:
155 OnOverridableCertError(main_frame_url, error); 152 OnOverridableCertError(error);
156 break; 153 break;
157 case net::ERR_CERT_NO_REVOCATION_MECHANISM: 154 case net::ERR_CERT_NO_REVOCATION_MECHANISM:
158 // Ignore this error. 155 // Ignore this error.
159 error->ContinueRequest(); 156 error->ContinueRequest();
160 break; 157 break;
161 case net::ERR_CERT_UNABLE_TO_CHECK_REVOCATION: 158 case net::ERR_CERT_UNABLE_TO_CHECK_REVOCATION:
162 // We ignore this error and display an infobar. 159 // We ignore this error and display an infobar.
163 error->ContinueRequest(); 160 error->ContinueRequest();
164 error->manager()->ShowMessage(l10n_util::GetString( 161 error->manager()->ShowMessage(l10n_util::GetString(
165 IDS_CERT_ERROR_UNABLE_TO_CHECK_REVOCATION_INFO_BAR)); 162 IDS_CERT_ERROR_UNABLE_TO_CHECK_REVOCATION_INFO_BAR));
166 break; 163 break;
167 case net::ERR_CERT_CONTAINS_ERRORS: 164 case net::ERR_CERT_CONTAINS_ERRORS:
168 case net::ERR_CERT_REVOKED: 165 case net::ERR_CERT_REVOKED:
169 case net::ERR_CERT_INVALID: 166 case net::ERR_CERT_INVALID:
170 OnFatalCertError(main_frame_url, error); 167 OnFatalCertError(error);
171 break; 168 break;
172 default: 169 default:
173 NOTREACHED(); 170 NOTREACHED();
174 error->CancelRequest(); 171 error->CancelRequest();
175 break; 172 break;
176 } 173 }
177 } 174 }
178 175
179 void SSLPolicy::OnMixedContent( 176 void SSLPolicy::OnMixedContent(SSLManager::MixedContentHandler* handler) {
180 NavigationController* navigation_controller, 177 // Get the user's mixed content preference.
181 const GURL& main_frame_url, 178 PrefService* prefs = handler->GetWebContents()->profile()->GetPrefs();
182 SSLManager::MixedContentHandler* mixed_content_handler) { 179 FilterPolicy::Type filter_policy =
183 PrefService* prefs = navigation_controller->profile()->GetPrefs(); 180 FilterPolicy::FromInt(prefs->GetInteger(prefs::kMixedContentFiltering));
184 FilterPolicy::Type filter_policy = FilterPolicy::DONT_FILTER; 181
185 if (!mixed_content_handler->manager()-> 182 // If the user have added an exception, doctor the |filter_policy|.
186 CanShowInsecureContent(main_frame_url)) { 183 if (handler->manager()->DidAllowMixedContentForHost(
187 filter_policy = FilterPolicy::FromInt( 184 GURL(handler->main_frame_origin()).host()))
188 prefs->GetInteger(prefs::kMixedContentFiltering)); 185 filter_policy = FilterPolicy::DONT_FILTER;
189 } 186
190 if (filter_policy != FilterPolicy::DONT_FILTER) { 187 if (filter_policy != FilterPolicy::DONT_FILTER) {
191 mixed_content_handler->manager()->ShowMessageWithLink( 188 handler->manager()->ShowMessageWithLink(
192 l10n_util::GetString(IDS_SSL_INFO_BAR_FILTERED_CONTENT), 189 l10n_util::GetString(IDS_SSL_INFO_BAR_FILTERED_CONTENT),
193 l10n_util::GetString(IDS_SSL_INFO_BAR_SHOW_CONTENT), 190 l10n_util::GetString(IDS_SSL_INFO_BAR_SHOW_CONTENT),
194 new ShowUnsafeContentTask(main_frame_url, mixed_content_handler)); 191 new ShowMixedContentTask(handler));
195 } 192 }
196 mixed_content_handler->StartRequest(filter_policy); 193 handler->StartRequest(filter_policy);
197 194
198 NavigationEntry* entry = navigation_controller->GetLastCommittedEntry(); 195 NavigationEntry* entry =
199 DCHECK(entry); 196 handler->manager()->controller()->GetLastCommittedEntry();
197 // We might not have a navigation entry in some cases (e.g. when a»
198 // HTTPS page opens a popup with no URL and then populate it with»
199 // document.write()). See bug http://crbug.com/3845.
200 if (!entry)
201 return;
202
200 // Even though we are loading the mixed-content resource, it will not be 203 // Even though we are loading the mixed-content resource, it will not be
201 // included in the page when we set the policy to FILTER_ALL or 204 // included in the page when we set the policy to FILTER_ALL or
202 // FILTER_ALL_EXCEPT_IMAGES (only images and they are stamped with warning 205 // FILTER_ALL_EXCEPT_IMAGES (only images and they are stamped with warning
203 // icons), so we don't set the mixed-content mode in these cases. 206 // icons), so we don't set the mixed-content mode in these cases.
204 if (filter_policy == FilterPolicy::DONT_FILTER) 207 if (filter_policy == FilterPolicy::DONT_FILTER)
205 entry->ssl().set_has_mixed_content(); 208 entry->ssl().set_has_mixed_content();
206 209
207 // Print a message indicating the mixed-contents resource in the console. 210 // Print a message indicating the mixed-contents resource in the console.
208 const std::wstring& msg = l10n_util::GetStringF( 211 const std::wstring& msg = l10n_util::GetStringF(
209 IDS_MIXED_CONTENT_LOG_MESSAGE, 212 IDS_MIXED_CONTENT_LOG_MESSAGE,
210 UTF8ToWide(entry->url().spec()), 213 UTF8ToWide(entry->url().spec()),
211 UTF8ToWide(mixed_content_handler->request_url().spec())); 214 UTF8ToWide(handler->request_url().spec()));
212 mixed_content_handler->manager()-> 215 handler->manager()->AddMessageToConsole(msg, MESSAGE_LEVEL_WARNING);
213 AddMessageToConsole(msg, MESSAGE_LEVEL_WARNING);
214 216
215 NotificationService::current()->Notify( 217 NotificationService::current()->Notify(
216 NotificationType::SSL_VISIBLE_STATE_CHANGED, 218 NotificationType::SSL_VISIBLE_STATE_CHANGED,
217 Source<NavigationController>(navigation_controller), 219 Source<NavigationController>(handler->manager()->controller()),
218 Details<NavigationEntry>(entry)); 220 Details<NavigationEntry>(entry));
219 } 221 }
220 222
221 void SSLPolicy::OnRequestStarted(SSLManager* manager, const GURL& url, 223 void SSLPolicy::OnRequestStarted(SSLManager::RequestInfo* info) {
222 ResourceType::Type resource_type,
223 int ssl_cert_id, int ssl_cert_status) {
224 // These schemes never leave the browser and don't require a warning. 224 // These schemes never leave the browser and don't require a warning.
225 if (url.SchemeIs(chrome::kDataScheme) || 225 if (info->url().SchemeIs(chrome::kDataScheme) ||
226 url.SchemeIs(chrome::kJavaScriptScheme) || 226 info->url().SchemeIs(chrome::kJavaScriptScheme) ||
227 url.SchemeIs(chrome::kAboutScheme)) 227 info->url().SchemeIs(chrome::kAboutScheme))
228 return; 228 return;
229 229
230 NavigationEntry* entry = manager->controller()->GetActiveEntry(); 230 NavigationEntry* entry = info->manager()->controller()->GetActiveEntry();
231 if (!entry) { 231 if (!entry) {
232 // We may not have an entry for cases such as the inspector. 232 // We may not have an entry for cases such as the inspector.
233 return; 233 return;
234 } 234 }
235 235
236 NavigationEntry::SSLStatus& ssl = entry->ssl(); 236 NavigationEntry::SSLStatus& ssl = entry->ssl();
237 bool changed = false; 237 bool changed = false;
238 if (!entry->url().SchemeIsSecure() || // Current page is not secure. 238 if (!entry->url().SchemeIsSecure() || // Current page is not secure.
239 resource_type == ResourceType::MAIN_FRAME || // Main frame load. 239 info->resource_type() == ResourceType::MAIN_FRAME || // Main frame load.
240 net::IsCertStatusError(ssl.cert_status())) { // There is already 240 net::IsCertStatusError(ssl.cert_status())) { // There is already
241 // an error for the main page, don't report sub-resources as unsafe 241 // an error for the main page, don't report sub-resources as unsafe
242 // content. 242 // content.
243 // No mixed/unsafe content check necessary. 243 // No mixed/unsafe content check necessary.
244 return; 244 return;
245 } 245 }
246 246
247 if (url.SchemeIsSecure()) { 247 if (info->url().SchemeIsSecure()) {
248 // Check for insecure content (anything served over intranet is considered 248 // Check for insecure content (anything served over intranet is considered
249 // insecure). 249 // insecure).
250 250
251 // TODO(jcampan): bug #1178228 Disabling the broken style for intranet 251 // TODO(jcampan): bug #1178228 Disabling the broken style for intranet
252 // hosts for beta as it is missing error strings (and cert status). 252 // hosts for beta as it is missing error strings (and cert status).
253 // if (IsIntranetHost(url.host()) || 253 // if (IsIntranetHost(url.host()) ||
254 // net::IsCertStatusError(ssl_cert_status)) { 254 // net::IsCertStatusError(info->ssl_cert_status())) {
255 if (net::IsCertStatusError(ssl_cert_status)) { 255 if (net::IsCertStatusError(info->ssl_cert_status())) {
256 // The resource is unsafe. 256 // The resource is unsafe.
257 if (!ssl.has_unsafe_content()) { 257 if (!ssl.has_unsafe_content()) {
258 changed = true; 258 changed = true;
259 ssl.set_has_unsafe_content(); 259 ssl.set_has_unsafe_content();
260 manager->SetMaxSecurityStyle(SECURITY_STYLE_AUTHENTICATION_BROKEN); 260 info->manager()->SetMaxSecurityStyle(
261 SECURITY_STYLE_AUTHENTICATION_BROKEN);
261 } 262 }
262 } 263 }
263 } 264 }
264 265
265 if (changed) { 266 if (changed) {
266 // Only send the notification when something actually changed. 267 // Only send the notification when something actually changed.
267 NotificationService::current()->Notify( 268 NotificationService::current()->Notify(
268 NotificationType::SSL_VISIBLE_STATE_CHANGED, 269 NotificationType::SSL_VISIBLE_STATE_CHANGED,
269 Source<NavigationController>(manager->controller()), 270 Source<NavigationController>(info->manager()->controller()),
270 NotificationService::NoDetails()); 271 NotificationService::NoDetails());
271 } 272 }
272 } 273 }
273 274
274 SecurityStyle SSLPolicy::GetDefaultStyle(const GURL& url) { 275 SecurityStyle SSLPolicy::GetDefaultStyle(const GURL& url) {
275 // Show the secure style for HTTPS. 276 // Show the secure style for HTTPS.
276 if (url.SchemeIsSecure()) { 277 if (url.SchemeIsSecure()) {
277 // TODO(jcampan): bug #1178228 Disabling the broken style for intranet 278 // TODO(jcampan): bug #1178228 Disabling the broken style for intranet
278 // hosts for beta as it is missing error strings (and cert status). 279 // hosts for beta as it is missing error strings (and cert status).
279 // CAs issue certs for intranet hosts to anyone. 280 // CAs issue certs for intranet hosts to anyone.
(...skipping 18 matching lines...) Expand all
298 299
299 // We can't possibly have mixed content when loading the main frame. 300 // We can't possibly have mixed content when loading the main frame.
300 if (resource_type == ResourceType::MAIN_FRAME) 301 if (resource_type == ResourceType::MAIN_FRAME)
301 return false; 302 return false;
302 303
303 // TODO(abarth): This is wrong, but it matches our current behavior. 304 // TODO(abarth): This is wrong, but it matches our current behavior.
304 // I'll fix this in a subsequent step. 305 // I'll fix this in a subsequent step.
305 return GURL(main_frame_origin).SchemeIsSecure() && !url.SchemeIsSecure(); 306 return GURL(main_frame_origin).SchemeIsSecure() && !url.SchemeIsSecure();
306 } 307 }
307 308
309 ////////////////////////////////////////////////////////////////////////////////
310 // SSLBlockingPage::Delegate methods
311
308 SSLErrorInfo SSLPolicy::GetSSLErrorInfo(SSLManager::CertError* error) { 312 SSLErrorInfo SSLPolicy::GetSSLErrorInfo(SSLManager::CertError* error) {
309 return SSLErrorInfo::CreateError( 313 return SSLErrorInfo::CreateError(
310 SSLErrorInfo::NetErrorToErrorType(error->cert_error()), 314 SSLErrorInfo::NetErrorToErrorType(error->cert_error()),
311 error->ssl_info().cert, error->request_url()); 315 error->ssl_info().cert, error->request_url());
312 } 316 }
313 317
314 void SSLPolicy::OnDenyCertificate(SSLManager::CertError* error) { 318 void SSLPolicy::OnDenyCertificate(SSLManager::CertError* error) {
315 // Default behavior for rejecting a certificate. 319 // Default behavior for rejecting a certificate.
316 error->CancelRequest(); 320 error->CancelRequest();
317 error->manager()->DenyCertForHost(error->ssl_info().cert, 321 error->manager()->DenyCertForHost(error->ssl_info().cert,
318 error->request_url().host()); 322 error->request_url().host());
319 } 323 }
320 324
321 void SSLPolicy::OnAllowCertificate(SSLManager::CertError* error) { 325 void SSLPolicy::OnAllowCertificate(SSLManager::CertError* error) {
322 // Default behavior for accepting a certificate. 326 // Default behavior for accepting a certificate.
323 // Note that we should not call SetMaxSecurityStyle here, because the active 327 // Note that we should not call SetMaxSecurityStyle here, because the active
324 // NavigationEntry has just been deleted (in HideInterstitialPage) and the 328 // NavigationEntry has just been deleted (in HideInterstitialPage) and the
325 // new NavigationEntry will not be set until DidNavigate. This is ok, 329 // new NavigationEntry will not be set until DidNavigate. This is ok,
326 // because the new NavigationEntry will have its max security style set 330 // because the new NavigationEntry will have its max security style set
327 // within DidNavigate. 331 // within DidNavigate.
328 error->ContinueRequest(); 332 error->ContinueRequest();
329 error->manager()->AllowCertForHost(error->ssl_info().cert, 333 error->manager()->AllowCertForHost(error->ssl_info().cert,
330 error->request_url().host()); 334 error->request_url().host());
331 } 335 }
332 336
333 void SSLPolicy::OnOverridableCertError(const GURL& main_frame_url, 337 ////////////////////////////////////////////////////////////////////////////////
334 SSLManager::CertError* error) { 338 // Certificate Error Routines
339
340 void SSLPolicy::OnOverridableCertError(SSLManager::CertError* error) {
335 if (error->resource_type() != ResourceType::MAIN_FRAME) { 341 if (error->resource_type() != ResourceType::MAIN_FRAME) {
336 // A sub-resource has a certificate error. The user doesn't really 342 // A sub-resource has a certificate error. The user doesn't really
337 // have a context for making the right decision, so block the 343 // have a context for making the right decision, so block the
338 // request hard, without an info bar to allow showing the insecure 344 // request hard, without an info bar to allow showing the insecure
339 // content. 345 // content.
340 error->DenyRequest(); 346 error->DenyRequest();
341 return; 347 return;
342 } 348 }
343 // We need to ask the user to approve this certificate. 349 // We need to ask the user to approve this certificate.
344 ShowBlockingPage(this, error); 350 ShowBlockingPage(this, error);
345 } 351 }
346 352
347 void SSLPolicy::OnFatalCertError(const GURL& main_frame_url, 353 void SSLPolicy::OnFatalCertError(SSLManager::CertError* error) {
348 SSLManager::CertError* error) {
349 if (error->resource_type() != ResourceType::MAIN_FRAME) { 354 if (error->resource_type() != ResourceType::MAIN_FRAME) {
350 error->DenyRequest(); 355 error->DenyRequest();
351 return; 356 return;
352 } 357 }
353 error->CancelRequest(); 358 error->CancelRequest();
354 ShowErrorPage(this, error); 359 ShowErrorPage(this, error);
355 // No need to degrade our security indicators because we didn't continue. 360 // No need to degrade our security indicators because we didn't continue.
356 } 361 }
OLDNEW
« no previous file with comments | « chrome/browser/ssl/ssl_policy.h ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698