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

Side by Side Diff: Source/modules/websockets/NewWebSocketChannelImpl.cpp

Issue 22914026: [ABANDONED] Introduce blink-side bridges for the new WebSocket implementation. (Closed) Base URL: https://chromium.googlesource.com/chromium/blink.git@master
Patch Set: Created 7 years, 3 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
« no previous file with comments | « Source/modules/websockets/NewWebSocketChannelImpl.h ('k') | public/platform/Platform.h » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 /*
2 * Copyright (C) 2013 Google Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
6 * met:
7 *
8 * * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
13 * distribution.
14 * * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31 #include "config.h"
32 #include "modules/websockets/NewWebSocketChannelImpl.h"
33
34 #include "bindings/v8/ScriptCallStackFactory.h"
35 #include "core/dom/ScriptExecutionContext.h"
36 #include "core/fileapi/Blob.h"
37 #include "core/fileapi/FileReaderLoader.h"
38 #include "core/inspector/InspectorInstrumentation.h"
39 #include "core/inspector/ScriptCallStack.h"
40 #include "core/loader/UniqueIdentifier.h"
41 #include "core/platform/Logging.h"
42 #include "modules/websockets/WebSocketChannel.h"
43 #include "modules/websockets/WebSocketChannelClient.h"
44 #include "public/platform/Platform.h"
45 #include "public/platform/WebData.h"
46 #include "public/platform/WebSocketHandle.h"
47 #include "public/platform/WebString.h"
48 #include "public/platform/WebURL.h"
49 #include "public/platform/WebVector.h"
50 #include "weborigin/SecurityOrigin.h"
51 #include "wtf/ArrayBuffer.h"
52 #include "wtf/OwnPtr.h"
53 #include "wtf/PassRefPtr.h"
54 #include "wtf/RefPtr.h"
55 #include "wtf/Vector.h"
56 #include "wtf/text/WTFString.h"
57
58 // FIXME: The following notifications are not implemented:
59 // InspectorInstrument::willSendWebSocketHandshake
60 // InspectorInstrument::didReceiveWebSocketHandshakeResponse
61 // InspectorInstrument::didReceiveWebSocketFrame
62 // InspectorInstrument::didSendWebSocketFrame
63
64 using WebKit::WebSocketHandle;
65
66 namespace WebCore {
67
68 namespace {
69
70 bool isClean(int code)
71 {
72 return code == WebSocketChannel::CloseEventCodeNormalClosure
73 || (WebSocketChannel::CloseEventCodeMinimumUserDefined <= code
74 && code <= WebSocketChannel::CloseEventCodeMaximumUserDefined);
75 }
76
77 } // namespace
78
79 NewWebSocketChannelImpl::NewWebSocketChannelImpl(ScriptExecutionContext* context , WebSocketChannelClient* client, const String& sourceURL, unsigned lineNumber)
80 : m_context(context)
81 , m_handle(adoptPtr(WebKit::Platform::current()->createWebSocketHandle()))
82 , m_client(client)
83 , m_identifier(0)
84 , m_state(NotConnected)
85 , m_sendingQuota(0)
86 , m_receivedDataSizeForFlowControl(0)
87 , m_bufferedAmount(0)
88 , m_sentSizeOfTopMessage(0)
89 , m_hasAlreadyFailed(false)
90 , m_isSuspended(false)
91 , m_resumeTimer(this, &NewWebSocketChannelImpl::resumeTimerFired)
92 , m_sourceURLAtConnection(sourceURL)
93 , m_lineNumberAtConnection(lineNumber)
94 {
95 if (context->isDocument() && toDocument(context)->page()) {
96 m_identifier = createUniqueIdentifier();
97 }
98 }
99
100 void NewWebSocketChannelImpl::connect(const KURL& url, const String& protocol)
101 {
102 ASSERT(m_state == NotConnected);
103 LOG(Network, "NewWebSocketChannelImpl %p connect()", this);
104 if (m_identifier) {
105 InspectorInstrumentation::didCreateWebSocket(toDocument(m_context), m_id entifier, url, protocol);
106 }
107 m_state = Connecting;
108 m_url = url;
109 Vector<String> protocols;
110 // Since protocol is already verified and escaped, we can simply split it.
111 protocol.split(", ", true, protocols);
112 WebKit::WebVector<WebKit::WebString> webProtocols(protocols.size());
113 for (size_t i = 0; i < protocols.size(); ++i) {
114 webProtocols[i] = protocols[i];
115 }
116 m_handle->connect(url, webProtocols, m_context->securityOrigin()->toString() , this);
117
118 RefPtr<ScriptCallStack> callStack = createScriptCallStack(1, true);
119 if (callStack && callStack->size()) {
120 m_sourceURLAtConnection = callStack->at(0).sourceURL();
121 m_lineNumberAtConnection = callStack->at(0).lineNumber();
122 }
123 }
124
125 String NewWebSocketChannelImpl::subprotocol()
126 {
127 LOG(Network, "NewWebSocketChannelImpl %p subprotocol()", this);
128 return m_subprotocol;
129 }
130
131 String NewWebSocketChannelImpl::extensions()
132 {
133 LOG(Network, "NewWebSocketChannelImpl %p extensions()", this);
134 return m_extensions;
135 }
136
137 WebSocketChannel::SendResult NewWebSocketChannelImpl::send(const String& message )
138 {
139 LOG(Network, "NewWebSocketChannelImpl %p sendText(%s)", this, message.utf8() .data());
140 if (m_state != Open) {
141 return SendFail;
142 }
143 m_messages.append(Message(message));
144 sendInternal();
145 return SendSuccess;
146 }
147
148 WebSocketChannel::SendResult NewWebSocketChannelImpl::send(const Blob& blob)
149 {
150 LOG(Network, "NewWebSocketChannelImpl %p sendBlob()", this);
151 if (m_state != Open) {
152 return SendFail;
153 }
154 m_messages.append(Message(blob));
155 sendInternal();
156 return SendSuccess;
157 }
158
159 WebSocketChannel::SendResult NewWebSocketChannelImpl::send(const ArrayBuffer& bu ffer, unsigned byteOffset, unsigned byteLength)
160 {
161 LOG(Network, "NewWebSocketChannelImpl %p sendArrayBuffer(%p, %u, %u)", this, buffer.data(), byteOffset, byteLength);
162 if (m_state != Open) {
163 return SendFail;
164 }
165 // buffer.slice copies its contents.
166 m_messages.append(buffer.slice(byteOffset, byteOffset + byteLength));
167 sendInternal();
168 return SendSuccess;
169 }
170
171 unsigned long NewWebSocketChannelImpl::bufferedAmount() const
172 {
173 LOG(Network, "NewWebSocketChannelImpl %p bufferedAmount()", this);
174 return m_bufferedAmount;
175 }
176
177 void NewWebSocketChannelImpl::close(int code, const String& reason)
178 {
179 LOG(Network, "NewWebSocketChannelImpl %p close(%d, %s)", this, code, reason. utf8().data());
180 if (m_state == Closing || m_state == Closed) {
181 return;
182 }
183 ASSERT(m_handle);
184 if (m_state == Open) {
185 m_handle->close(static_cast<unsigned short>(code), reason);
186 }
187 m_state = Closing;
188 }
189
190 void NewWebSocketChannelImpl::fail(const String& reason, MessageLevel level, con st String& sourceURL, unsigned lineNumber)
191 {
192 LOG(Network, "NewWebSocketChannelImpl %p fail(%s)", this, reason.utf8().data ());
193 // m_handle and m_client can be null here.
194 const String message = "WebSocket connection to '" + m_url.elidedString() + "' failed: " + reason;
195 m_context->addConsoleMessage(JSMessageSource, level, message, sourceURL, lin eNumber);
196 if (m_identifier) {
197 InspectorInstrumentation::didReceiveWebSocketFrameError(toDocument(m_con text), m_identifier, reason);
198 }
199
200 if (m_client && !m_hasAlreadyFailed) {
201 if (m_isSuspended) {
202 m_pendingEvents.append(PendingEvent(PendingEvent::DidReceiveError));
203 } else {
204 m_client->didReceiveMessageError();
205 }
206 }
207 m_hasAlreadyFailed = true;
208 if (m_state != Closing && m_state != Closed) {
209 disconnect();
210 }
211 }
212
213 void NewWebSocketChannelImpl::disconnect()
214 {
215 LOG(Network, "NewWebSocketChannelImpl %p disconnect()", this);
216 if (m_state == Closed) {
217 return;
218 }
219 if (m_identifier) {
220 InspectorInstrumentation::didCloseWebSocket(toDocument(m_context), m_ide ntifier);
221 }
222 ASSERT(m_handle);
223 if (m_state != Closing) {
224 m_handle->close(CloseEventCodeAbnormalClosure, "");
225 }
226 m_state = Closed;
227 m_handle = 0;
228 m_client = 0;
229 }
230
231 void NewWebSocketChannelImpl::suspend()
232 {
233 LOG(Network, "NewWebSocketChannelImpl %p suspend()", this);
234 m_isSuspended = true;
235 }
236
237 void NewWebSocketChannelImpl::resume()
238 {
239 LOG(Network, "NewWebSocketChannelImpl %p resume()", this);
240 m_isSuspended = false;
241 // Use a timer to finish this function quickly.
242 if (!m_resumeTimer.isActive()) {
243 // Protect this object until the timer fires.
244 ref();
245 m_resumeTimer.startOneShot(0);
246 }
247 }
248
249 NewWebSocketChannelImpl::Message::Message(const String& text)
250 : type(MessageTypeText)
251 , text(text.utf8(String::StrictConversionReplacingUnpairedSurrogatesWithFFFD )) { }
252
253 NewWebSocketChannelImpl::Message::Message(const Blob& blob)
254 : type(MessageTypeBlob)
255 , blob(Blob::create(blob.url(), blob.type(), blob.size())) { }
256
257 NewWebSocketChannelImpl::Message::Message(PassRefPtr<ArrayBuffer> arrayBuffer)
258 : type(MessageTypeArrayBuffer)
259 , arrayBuffer(arrayBuffer) { }
260
261 void NewWebSocketChannelImpl::sendInternal()
262 {
263 if (m_state != Open || m_blobLoader || !m_sendingQuota) {
264 return;
265 }
266 ASSERT(m_handle);
267 ASSERT(m_client);
268 unsigned long bufferedAmount = m_bufferedAmount;
269 size_t i;
270 for (i = 0; i < m_messages.size(); ++i) {
271 if (!m_sendingQuota) {
272 break;
273 }
274 const Message& message = m_messages[i];
275 switch (message.type) {
276 case MessageTypeText: {
277 WebSocketHandle::MessageType type =
278 m_sentSizeOfTopMessage ? WebSocketHandle::MessageTypeContinuatio n : WebSocketHandle::MessageTypeText;
279 size_t size = std::min(static_cast<size_t>(m_sendingQuota), message. text.length() - m_sentSizeOfTopMessage);
yhirano 2013/08/28 02:11:36 If m_sendingQuota == message.text.length - m_sentS
yhirano 2013/08/28 02:14:45 Done.
280 m_handle->send(type, message.text.data() + m_sentSizeOfTopMessage, s ize, m_sendingQuota == size);
281 m_sentSizeOfTopMessage += size;
282 m_sendingQuota -= size;
283 break;
284 }
285 case MessageTypeBlob:
286 startLoadingBlob(*message.blob);
287 break;
288 case MessageTypeArrayBuffer: {
289 WebSocketHandle::MessageType type =
290 m_sentSizeOfTopMessage ? WebSocketHandle::MessageTypeContinuatio n : WebSocketHandle::MessageTypeBinary;
291 size_t size = std::min(static_cast<size_t>(m_sendingQuota), message. arrayBuffer->byteLength() - m_sentSizeOfTopMessage);
292 m_handle->send(type, static_cast<const char*>(message.arrayBuffer->d ata()) + m_sentSizeOfTopMessage, size, m_sendingQuota == size);
293 m_sentSizeOfTopMessage += size;
294 m_sendingQuota -= size;
295 break;
296 }
297 default:
298 ASSERT_NOT_REACHED();
299 }
300 if (m_blobLoader || !m_sendingQuota) {
301 break;
302 }
303
304 m_sentSizeOfTopMessage = 0;
305 }
306 // Drop consumed messages.
307 for (size_t j = 0; j < i; ++j) {
308 m_messages.removeFirst();
309 }
310 if (!m_isSuspended && m_bufferedAmount != bufferedAmount) {
311 m_client->didUpdateBufferedAmount(m_bufferedAmount);
312 }
313 }
314
315 void NewWebSocketChannelImpl::flowControlIfNecessary()
316 {
317 if (m_state != Open) {
318 return;
319 }
320 ASSERT(m_handle);
321 if (m_receivedDataSizeForFlowControl < receivedDataSizeForFlowControlHighWat erMark) {
322 return;
323 }
324 m_handle->flowControl(m_receivedDataSizeForFlowControl);
325 m_receivedDataSizeForFlowControl = 0;
326 }
327
328 void NewWebSocketChannelImpl::didConnect(WebSocketHandle* handle, bool succeed, const WebKit::WebString& selectedProtocol, const WebKit::WebString& extensions)
329 {
330 LOG(Network, "NewWebSocketChannelImpl %p didConnect(%p, %d, %s, %s)", this, handle, succeed, selectedProtocol.utf8().data(), extensions.utf8().data());
331 if (m_state != Connecting) {
332 return;
333 }
334 ASSERT(handle == m_handle);
335 ASSERT(m_client);
336 if (!succeed) {
337 failAsError("Cannot connect to " + m_url.string() + ".");
338 return;
339 }
340 m_state = Open;
341 m_subprotocol = selectedProtocol;
342 m_extensions = extensions;
343 if (m_isSuspended) {
344 m_pendingEvents.append(PendingEvent(PendingEvent::DidConnectComplete));
345 } else {
346 m_client->didConnect();
347 }
348 }
349
350 void NewWebSocketChannelImpl::didReceiveData(WebSocketHandle* handle, WebSocketH andle::MessageType type, const char* data, size_t size, bool fin)
351 {
352 LOG(Network, "NewWebSocketChannelImpl %p didReceiveData(%p, %d, (%p, %zu), % d)", this, handle, type, data.data(), data.size(), fin);
353 if (m_state != Open) {
354 return;
355 }
356 ASSERT(handle == m_handle);
357 ASSERT(m_client);
358 switch (type) {
359 case WebSocketHandle::MessageTypeText:
360 ASSERT(m_receivingMessageData.isEmpty());
361 m_receivingMessageTypeIsText = true;
362 break;
363 case WebSocketHandle::MessageTypeBinary:
364 ASSERT(m_receivingMessageData.isEmpty());
365 m_receivingMessageTypeIsText = false;
366 break;
367 case WebSocketHandle::MessageTypeContinuation:
368 ASSERT(!m_receivingMessageData.isEmpty());
tyoshino (SeeGerritForStatus) 2013/08/27 09:15:39 it's possible that the first frame doesn't contain
yhirano 2013/08/28 02:14:45 As stated in websocket_messages.h[1], non-final fr
369 break;
370 default:
371 ASSERT_NOT_REACHED();
372 break;
373 }
374 m_receivingMessageData.append(data, size);
375 m_receivedDataSizeForFlowControl += size;
376 flowControlIfNecessary();
377 if (!fin) {
378 return;
379 }
380 if (m_isSuspended) {
381 m_pendingEvents.append(PendingEvent(m_receivingMessageTypeIsText ? Pendi ngEvent::DidReceiveTextMessage : PendingEvent::DidReceiveBinaryMessage));
382 m_pendingEvents.last().message.swap(m_receivingMessageData);
383 } else {
384 if (m_receivingMessageTypeIsText) {
tyoshino (SeeGerritForStatus) 2013/08/27 09:15:39 could you try factoring out common code here and i
yhirano 2013/08/28 02:14:45 Done.
385 String message = "";
386 if (m_receivingMessageData.size() > 0) {
387 message = String::fromUTF8(m_receivingMessageData.data(), m_rece ivingMessageData.size());
388 }
389 if (message.isNull()) {
390 failAsError("Could not decode a text frame as UTF-8.");
391 } else {
392 m_client->didReceiveMessage(message);
393 }
394 } else {
tyoshino (SeeGerritForStatus) 2013/08/27 09:15:39 ditto
yhirano 2013/08/28 02:14:45 Done.
395 OwnPtr<Vector<char> > binaryData = adoptPtr(new Vector<char>);
396 m_receivingMessageData.swap(*binaryData);
397 m_client->didReceiveBinaryData(binaryData.release());
398 }
399 }
400 m_receivingMessageData.clear();
401 }
402
403 void NewWebSocketChannelImpl::didClose(WebSocketHandle* handle, unsigned short c ode, const WebKit::WebString& reason)
404 {
405 // FIXME: Maybe we should notify an error to m_client for some didClose mess ages.
406 LOG(Network, "NewWebSocketChannelImpl %p didClose(%p, %d, %s)", this, code, String(reason).utf8().data());
407 if (m_state == Closed) {
408 return;
409 }
410 if (m_identifier) {
411 InspectorInstrumentation::didCloseWebSocket(toDocument(m_context), m_ide ntifier);
412 }
413 ASSERT(handle == m_handle);
414 m_handle = 0;
415 m_state = Closed;
416 if (m_isSuspended) {
417 m_pendingEvents.append(PendingEvent(code, reason));
418 return;
419 }
420 WebSocketChannelClient* client = m_client;
tyoshino (SeeGerritForStatus) 2013/08/27 09:15:39 ditto
yhirano 2013/08/28 02:14:45 Sorry, what does this comment mean?
tyoshino (SeeGerritForStatus) 2013/08/28 05:26:03 There's similar code at L523-525. So, I'd like you
421 m_client = 0;
422 ASSERT(client);
423 WebSocketChannelClient::ClosingHandshakeCompletionStatus status =
424 isClean(code) ? WebSocketChannelClient::ClosingHandshakeComplete : WebSo cketChannelClient::ClosingHandshakeIncomplete;
425 client->didClose(m_bufferedAmount, status, code, reason);
426 // client->didClose may delete this object.
427 }
428
429 void NewWebSocketChannelImpl::didFinishLoading()
430 {
431 // m_client can be invalid here.
432 LOG(Network, "NewWebSocketChannelImpl %p didFinishLoading()", this);
433 if (m_state == Open) {
434 ASSERT(m_handle);
435 // The loaded blob is always placed on m_messages[0].
436 ASSERT(m_messages.size() > 0 && m_messages[0].type == MessageTypeBlob);
437 // We replace it with the loaded blob.
438 m_messages[0] = Message(m_blobLoader->arrayBufferResult());
439 sendInternal();
440 }
441 m_blobLoader.clear();
442
443 deref();
444 // deref() may delete this object.
445 }
446
447 void NewWebSocketChannelImpl::didFail(FileError::ErrorCode errorCode)
448 {
449 // m_client can be invalid here.
450 LOG(Network, "NewWebSocketChannelImpl %p didFail(%d)", this, errorCode);
451 m_blobLoader.clear();
452 failAsError("Failed to load Blob: error code = " + String::number(errorCode) ); // FIXME: Generate human-friendly reason message.
453 deref();
454 // deref() may delete this object.
455 }
456
457 void NewWebSocketChannelImpl::resumeTimerFired(Timer<NewWebSocketChannelImpl>*)
458 {
459 RefPtr<NewWebSocketChannelImpl> protect(this);
460 deref();
461
462 if (!m_client) {
463 ASSERT(m_state == Closed);
464 ASSERT(m_pendingEvents.isEmpty());
465 return;
466 }
467 if (m_isSuspended) {
468 return;
469 }
470 sendInternal();
471 flowControlIfNecessary();
472 processPendingEvents();
473 }
474
475 void NewWebSocketChannelImpl::startLoadingBlob(const Blob& blob)
476 {
477 LOG(Network, "NewWebSocketChannelImpl %p startLoadingBlob(%s)", this, blob.u rl().string().utf8().data());
478 ASSERT(!m_blobLoader);
479 // Protect this object until the loading completes or fails.
480 ref();
481
482 m_blobLoader = adoptPtr(new FileReaderLoader(FileReaderLoader::ReadAsArrayBu ffer, this));
483 m_blobLoader->start(m_context, blob);
484 }
485
486 void NewWebSocketChannelImpl::processPendingEvents()
487 {
488 RefPtr<NewWebSocketChannelImpl> protect(this);
489 ASSERT(!m_isSuspended);
490
491 for (size_t i = 0; i < m_pendingEvents.size(); ++i) {
492 ASSERT(m_client);
493 PendingEvent& event = m_pendingEvents[i];
494 switch (event.type) {
495 case PendingEvent::DidConnectComplete:
496 m_client->didConnect();
497 break;
498 case PendingEvent::DidReceiveTextMessage: {
499 String message = "";
500 if (event.message.size() > 0) {
501 message = String::fromUTF8(event.message.data(), event.message.s ize());
502 }
503 if (message.isNull()) {
504 failAsError("Could not decode a text frame as UTF-8.");
505 // m_client can be null here.
506 } else {
507 m_client->didReceiveMessage(message);
508 }
509 break;
510 }
511 case PendingEvent::DidReceiveBinaryMessage: {
512 OwnPtr<Vector<char> > binaryData = adoptPtr(new Vector<char>);
513 event.message.swap(*binaryData);
514 m_client->didReceiveBinaryData(binaryData.release());
515 break;
516 }
517 case PendingEvent::DidReceiveError:
518 m_client->didReceiveMessageError();
519 break;
520 case PendingEvent::DidClose: {
521 ASSERT(m_state == Closed);
522 ASSERT(!m_handle);
523 WebSocketChannelClient::ClosingHandshakeCompletionStatus status =
524 isClean(event.closingCode) ? WebSocketChannelClient::ClosingHand shakeComplete : WebSocketChannelClient::ClosingHandshakeIncomplete;
525 m_client->didClose(m_bufferedAmount, status, event.closingCode, even t.closingReason);
526 // m_client can be invalid here.
527 m_client = 0;
528 break;
529 }
530 default:
531 ASSERT_NOT_REACHED();
532 break;
533 }
534
535 if (event.type == PendingEvent::DidClose || !m_client) {
536 // Drop remaining messages.
537 break;
538 }
539 }
540 m_pendingEvents.clear();
541 }
542
543 } // namespace WebCore
OLDNEW
« no previous file with comments | « Source/modules/websockets/NewWebSocketChannelImpl.h ('k') | public/platform/Platform.h » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698