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

Unified Diff: net/quic/quic_framer.cc

Issue 446063005: Move Quic AppendTimestampFrame method out of AppendCongestionFrame. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: 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 side-by-side diff with in-line comments
Download patch
« no previous file with comments | « net/quic/quic_framer.h ('k') | net/quic/quic_framer_test.cc » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: net/quic/quic_framer.cc
diff --git a/net/quic/quic_framer.cc b/net/quic/quic_framer.cc
index 2ef5c7fbdf1d8390ddc3d1d0e72ffe44a84df42b..c21bd6b72a1a19187e5fbfb9be5a65f846d4d0e6 100644
--- a/net/quic/quic_framer.cc
+++ b/net/quic/quic_framer.cc
@@ -972,219 +972,224 @@ QuicFramer::AckFrameInfo QuicFramer::GetAckFrameInfo(
++iter;
for (; iter != frame.missing_packets.end(); ++iter) {
if (cur_range_length != numeric_limits<uint8>::max() &&
- *iter == (last_missing + 1)) {
- ++cur_range_length;
- } else {
- ack_info.nack_ranges[last_missing - cur_range_length]
- = cur_range_length;
- cur_range_length = 0;
+ *iter == (last_missing + 1)) {
+ ++cur_range_length;
+ } else {
+ ack_info.nack_ranges[last_missing - cur_range_length]
+ = cur_range_length;
+ cur_range_length = 0;
+ }
+ ack_info.max_delta = max(ack_info.max_delta, *iter - last_missing);
+ last_missing = *iter;
+ }
+ // Include the last nack range.
+ ack_info.nack_ranges[last_missing - cur_range_length] =
+ cur_range_length;
+ // Include the range to the largest observed.
+ ack_info.max_delta = max(ack_info.max_delta,
+ frame.largest_observed - last_missing);
}
- ack_info.max_delta = max(ack_info.max_delta, *iter - last_missing);
- last_missing = *iter;
+ return ack_info;
}
- // Include the last nack range.
- ack_info.nack_ranges[last_missing - cur_range_length] = cur_range_length;
- // Include the range to the largest observed.
- ack_info.max_delta = max(ack_info.max_delta,
- frame.largest_observed - last_missing);
- }
- return ack_info;
-}
-bool QuicFramer::ProcessPacketHeader(
- QuicPacketHeader* header,
- const QuicEncryptedPacket& packet) {
- if (!ProcessPacketSequenceNumber(header->public_header.sequence_number_length,
- &header->packet_sequence_number)) {
- set_detailed_error("Unable to read sequence number.");
- return RaiseError(QUIC_INVALID_PACKET_HEADER);
- }
+ bool QuicFramer::ProcessPacketHeader(
+ QuicPacketHeader* header,
+ const QuicEncryptedPacket& packet) {
+ if (!ProcessPacketSequenceNumber(
+ header->public_header.sequence_number_length,
+ &header->packet_sequence_number)) {
+ set_detailed_error("Unable to read sequence number.");
+ return RaiseError(QUIC_INVALID_PACKET_HEADER);
+ }
- if (header->packet_sequence_number == 0u) {
- set_detailed_error("Packet sequence numbers cannot be 0.");
- return RaiseError(QUIC_INVALID_PACKET_HEADER);
- }
+ if (header->packet_sequence_number == 0u) {
+ set_detailed_error("Packet sequence numbers cannot be 0.");
+ return RaiseError(QUIC_INVALID_PACKET_HEADER);
+ }
- if (!visitor_->OnUnauthenticatedHeader(*header)) {
- return false;
- }
+ if (!visitor_->OnUnauthenticatedHeader(*header)) {
+ return false;
+ }
- if (!DecryptPayload(*header, packet)) {
- set_detailed_error("Unable to decrypt payload.");
- return RaiseError(QUIC_DECRYPTION_FAILURE);
- }
+ if (!DecryptPayload(*header, packet)) {
+ set_detailed_error("Unable to decrypt payload.");
+ return RaiseError(QUIC_DECRYPTION_FAILURE);
+ }
- uint8 private_flags;
- if (!reader_->ReadBytes(&private_flags, 1)) {
- set_detailed_error("Unable to read private flags.");
- return RaiseError(QUIC_INVALID_PACKET_HEADER);
- }
+ uint8 private_flags;
+ if (!reader_->ReadBytes(&private_flags, 1)) {
+ set_detailed_error("Unable to read private flags.");
+ return RaiseError(QUIC_INVALID_PACKET_HEADER);
+ }
- if (private_flags > PACKET_PRIVATE_FLAGS_MAX) {
- set_detailed_error("Illegal private flags value.");
- return RaiseError(QUIC_INVALID_PACKET_HEADER);
- }
+ if (private_flags > PACKET_PRIVATE_FLAGS_MAX) {
+ set_detailed_error("Illegal private flags value.");
+ return RaiseError(QUIC_INVALID_PACKET_HEADER);
+ }
- header->entropy_flag = (private_flags & PACKET_PRIVATE_FLAGS_ENTROPY) != 0;
- header->fec_flag = (private_flags & PACKET_PRIVATE_FLAGS_FEC) != 0;
+ header->entropy_flag =
+ (private_flags & PACKET_PRIVATE_FLAGS_ENTROPY) != 0;
+ header->fec_flag = (private_flags & PACKET_PRIVATE_FLAGS_FEC) != 0;
+
+ if ((private_flags & PACKET_PRIVATE_FLAGS_FEC_GROUP) != 0) {
+ header->is_in_fec_group = IN_FEC_GROUP;
+ uint8 first_fec_protected_packet_offset;
+ if (!reader_->ReadBytes(&first_fec_protected_packet_offset, 1)) {
+ set_detailed_error(
+ "Unable to read first fec protected packet offset.");
+ return RaiseError(QUIC_INVALID_PACKET_HEADER);
+ }
+ if (first_fec_protected_packet_offset >=
+ header->packet_sequence_number) {
+ set_detailed_error("First fec protected packet offset must be less "
+ "than the sequence number.");
+ return RaiseError(QUIC_INVALID_PACKET_HEADER);
+ }
+ header->fec_group =
+ header->packet_sequence_number - first_fec_protected_packet_offset;
+ }
- if ((private_flags & PACKET_PRIVATE_FLAGS_FEC_GROUP) != 0) {
- header->is_in_fec_group = IN_FEC_GROUP;
- uint8 first_fec_protected_packet_offset;
- if (!reader_->ReadBytes(&first_fec_protected_packet_offset, 1)) {
- set_detailed_error("Unable to read first fec protected packet offset.");
- return RaiseError(QUIC_INVALID_PACKET_HEADER);
- }
- if (first_fec_protected_packet_offset >= header->packet_sequence_number) {
- set_detailed_error("First fec protected packet offset must be less "
- "than the sequence number.");
- return RaiseError(QUIC_INVALID_PACKET_HEADER);
+ header->entropy_hash = GetPacketEntropyHash(*header);
+ // Set the last sequence number after we have decrypted the packet
+ // so we are confident is not attacker controlled.
+ last_sequence_number_ = header->packet_sequence_number;
+ return true;
}
- header->fec_group =
- header->packet_sequence_number - first_fec_protected_packet_offset;
- }
- header->entropy_hash = GetPacketEntropyHash(*header);
- // Set the last sequence number after we have decrypted the packet
- // so we are confident is not attacker controlled.
- last_sequence_number_ = header->packet_sequence_number;
- return true;
-}
-
-bool QuicFramer::ProcessPacketSequenceNumber(
- QuicSequenceNumberLength sequence_number_length,
- QuicPacketSequenceNumber* sequence_number) {
- QuicPacketSequenceNumber wire_sequence_number = 0u;
- if (!reader_->ReadBytes(&wire_sequence_number, sequence_number_length)) {
- return false;
- }
-
- // TODO(ianswett): Explore the usefulness of trying multiple sequence numbers
- // in case the first guess is incorrect.
- *sequence_number =
- CalculatePacketSequenceNumberFromWire(sequence_number_length,
- wire_sequence_number);
- return true;
-}
+ bool QuicFramer::ProcessPacketSequenceNumber(
+ QuicSequenceNumberLength sequence_number_length,
+ QuicPacketSequenceNumber* sequence_number) {
+ QuicPacketSequenceNumber wire_sequence_number = 0u;
+ if (!reader_->ReadBytes(&wire_sequence_number, sequence_number_length)) {
+ return false;
+ }
-bool QuicFramer::ProcessFrameData(const QuicPacketHeader& header) {
- if (reader_->IsDoneReading()) {
- set_detailed_error("Packet has no frames.");
- return RaiseError(QUIC_MISSING_PAYLOAD);
- }
- while (!reader_->IsDoneReading()) {
- uint8 frame_type;
- if (!reader_->ReadBytes(&frame_type, 1)) {
- set_detailed_error("Unable to read frame type.");
- return RaiseError(QUIC_INVALID_FRAME_DATA);
+ // TODO(ianswett): Explore the usefulness of trying multiple sequence
+ // numbers in case the first guess is incorrect.
+ *sequence_number =
+ CalculatePacketSequenceNumberFromWire(sequence_number_length,
+ wire_sequence_number);
+ return true;
}
- if (frame_type & kQuicFrameTypeSpecialMask) {
- // Stream Frame
- if (frame_type & kQuicFrameTypeStreamMask) {
- QuicStreamFrame frame;
- if (!ProcessStreamFrame(frame_type, &frame)) {
- return RaiseError(QUIC_INVALID_STREAM_DATA);
- }
- if (!visitor_->OnStreamFrame(frame)) {
- DVLOG(1) << "Visitor asked to stop further processing.";
- // Returning true since there was no parsing error.
- return true;
- }
- continue;
+ bool QuicFramer::ProcessFrameData(const QuicPacketHeader& header) {
+ if (reader_->IsDoneReading()) {
+ set_detailed_error("Packet has no frames.");
+ return RaiseError(QUIC_MISSING_PAYLOAD);
}
-
- // Ack Frame
- if (frame_type & kQuicFrameTypeAckMask) {
- QuicAckFrame frame;
- if (!ProcessAckFrame(frame_type, &frame)) {
- return RaiseError(QUIC_INVALID_ACK_DATA);
- }
- if (!visitor_->OnAckFrame(frame)) {
- DVLOG(1) << "Visitor asked to stop further processing.";
- // Returning true since there was no parsing error.
- return true;
+ while (!reader_->IsDoneReading()) {
+ uint8 frame_type;
+ if (!reader_->ReadBytes(&frame_type, 1)) {
+ set_detailed_error("Unable to read frame type.");
+ return RaiseError(QUIC_INVALID_FRAME_DATA);
}
- continue;
- }
- // Congestion Feedback Frame
- if (frame_type & kQuicFrameTypeCongestionFeedbackMask) {
- QuicCongestionFeedbackFrame frame;
- if (!ProcessQuicCongestionFeedbackFrame(&frame)) {
- return RaiseError(QUIC_INVALID_CONGESTION_FEEDBACK_DATA);
- }
- if (!visitor_->OnCongestionFeedbackFrame(frame)) {
- DVLOG(1) << "Visitor asked to stop further processing.";
- // Returning true since there was no parsing error.
- return true;
- }
- continue;
- }
+ if (frame_type & kQuicFrameTypeSpecialMask) {
+ // Stream Frame
+ if (frame_type & kQuicFrameTypeStreamMask) {
+ QuicStreamFrame frame;
+ if (!ProcessStreamFrame(frame_type, &frame)) {
+ return RaiseError(QUIC_INVALID_STREAM_DATA);
+ }
+ if (!visitor_->OnStreamFrame(frame)) {
+ DVLOG(1) << "Visitor asked to stop further processing.";
+ // Returning true since there was no parsing error.
+ return true;
+ }
+ continue;
+ }
- // This was a special frame type that did not match any
- // of the known ones. Error.
- set_detailed_error("Illegal frame type.");
- DLOG(WARNING) << "Illegal frame type: "
- << static_cast<int>(frame_type);
- return RaiseError(QUIC_INVALID_FRAME_DATA);
- }
+ // Ack Frame
+ if (frame_type & kQuicFrameTypeAckMask) {
+ QuicAckFrame frame;
+ if (!ProcessAckFrame(frame_type, &frame)) {
+ return RaiseError(QUIC_INVALID_ACK_DATA);
+ }
+ if (!visitor_->OnAckFrame(frame)) {
+ DVLOG(1) << "Visitor asked to stop further processing.";
+ // Returning true since there was no parsing error.
+ return true;
+ }
+ continue;
+ }
- switch (frame_type) {
- case PADDING_FRAME:
- // We're done with the packet.
- return true;
+ // Congestion Feedback Frame
+ if (frame_type & kQuicFrameTypeCongestionFeedbackMask) {
+ QuicCongestionFeedbackFrame frame;
+ if (!ProcessQuicCongestionFeedbackFrame(&frame)) {
+ return RaiseError(QUIC_INVALID_CONGESTION_FEEDBACK_DATA);
+ }
+ if (!visitor_->OnCongestionFeedbackFrame(frame)) {
+ DVLOG(1) << "Visitor asked to stop further processing.";
+ // Returning true since there was no parsing error.
+ return true;
+ }
+ continue;
+ }
- case RST_STREAM_FRAME: {
- QuicRstStreamFrame frame;
- if (!ProcessRstStreamFrame(&frame)) {
- return RaiseError(QUIC_INVALID_RST_STREAM_DATA);
+ // This was a special frame type that did not match any
+ // of the known ones. Error.
+ set_detailed_error("Illegal frame type.");
+ DLOG(WARNING) << "Illegal frame type: "
+ << static_cast<int>(frame_type);
+ return RaiseError(QUIC_INVALID_FRAME_DATA);
}
- if (!visitor_->OnRstStreamFrame(frame)) {
- DVLOG(1) << "Visitor asked to stop further processing.";
- // Returning true since there was no parsing error.
- return true;
- }
- continue;
- }
- case CONNECTION_CLOSE_FRAME: {
- QuicConnectionCloseFrame frame;
- if (!ProcessConnectionCloseFrame(&frame)) {
- return RaiseError(QUIC_INVALID_CONNECTION_CLOSE_DATA);
- }
+ switch (frame_type) {
+ case PADDING_FRAME:
+ // We're done with the packet.
+ return true;
+
+ case RST_STREAM_FRAME: {
+ QuicRstStreamFrame frame;
+ if (!ProcessRstStreamFrame(&frame)) {
+ return RaiseError(QUIC_INVALID_RST_STREAM_DATA);
+ }
+ if (!visitor_->OnRstStreamFrame(frame)) {
+ DVLOG(1) << "Visitor asked to stop further processing.";
+ // Returning true since there was no parsing error.
+ return true;
+ }
+ continue;
+ }
- if (!visitor_->OnConnectionCloseFrame(frame)) {
- DVLOG(1) << "Visitor asked to stop further processing.";
- // Returning true since there was no parsing error.
- return true;
- }
- continue;
- }
+ case CONNECTION_CLOSE_FRAME: {
+ QuicConnectionCloseFrame frame;
+ if (!ProcessConnectionCloseFrame(&frame)) {
+ return RaiseError(QUIC_INVALID_CONNECTION_CLOSE_DATA);
+ }
+
+ if (!visitor_->OnConnectionCloseFrame(frame)) {
+ DVLOG(1) << "Visitor asked to stop further processing.";
+ // Returning true since there was no parsing error.
+ return true;
+ }
+ continue;
+ }
- case GOAWAY_FRAME: {
- QuicGoAwayFrame goaway_frame;
- if (!ProcessGoAwayFrame(&goaway_frame)) {
- return RaiseError(QUIC_INVALID_GOAWAY_DATA);
- }
- if (!visitor_->OnGoAwayFrame(goaway_frame)) {
- DVLOG(1) << "Visitor asked to stop further processing.";
- // Returning true since there was no parsing error.
- return true;
- }
- continue;
- }
+ case GOAWAY_FRAME: {
+ QuicGoAwayFrame goaway_frame;
+ if (!ProcessGoAwayFrame(&goaway_frame)) {
+ return RaiseError(QUIC_INVALID_GOAWAY_DATA);
+ }
+ if (!visitor_->OnGoAwayFrame(goaway_frame)) {
+ DVLOG(1) << "Visitor asked to stop further processing.";
+ // Returning true since there was no parsing error.
+ return true;
+ }
+ continue;
+ }
- case WINDOW_UPDATE_FRAME: {
- QuicWindowUpdateFrame window_update_frame;
- if (!ProcessWindowUpdateFrame(&window_update_frame)) {
- return RaiseError(QUIC_INVALID_WINDOW_UPDATE_DATA);
- }
- if (!visitor_->OnWindowUpdateFrame(window_update_frame)) {
- DVLOG(1) << "Visitor asked to stop further processing.";
- // Returning true since there was no parsing error.
- return true;
- }
+ case WINDOW_UPDATE_FRAME: {
+ QuicWindowUpdateFrame window_update_frame;
+ if (!ProcessWindowUpdateFrame(&window_update_frame)) {
+ return RaiseError(QUIC_INVALID_WINDOW_UPDATE_DATA);
+ }
+ if (!visitor_->OnWindowUpdateFrame(window_update_frame)) {
+ DVLOG(1) << "Visitor asked to stop further processing.";
+ // Returning true since there was no parsing error.
+ return true;
+ }
continue;
}
@@ -1417,9 +1422,8 @@ bool QuicFramer::ProcessQuicCongestionFeedbackFrame(
static_cast<CongestionFeedbackType>(feedback_type);
switch (frame->type) {
- case kInterArrival: {
- CongestionFeedbackMessageInterArrival* inter_arrival =
- &frame->inter_arrival;
+ case kTimestamp: {
+ CongestionFeedbackMessageTimestamp* timestamp = &frame->timestamp;
uint8 num_received_packets;
if (!reader_->ReadBytes(&num_received_packets, 1)) {
set_detailed_error("Unable to read num received packets.");
@@ -1442,7 +1446,7 @@ bool QuicFramer::ProcessQuicCongestionFeedbackFrame(
QuicTime time_received = creation_time_.Add(
QuicTime::Delta::FromMicroseconds(time_received_us));
- inter_arrival->received_packet_times.insert(
+ timestamp->received_packet_times.insert(
make_pair(smallest_received, time_received));
for (uint8 i = 0; i < num_received_packets - 1; ++i) {
@@ -1460,7 +1464,7 @@ bool QuicFramer::ProcessQuicCongestionFeedbackFrame(
return false;
}
QuicPacketSequenceNumber packet = smallest_received + sequence_delta;
- inter_arrival->received_packet_times.insert(
+ timestamp->received_packet_times.insert(
make_pair(packet, time_received.Add(
QuicTime::Delta::FromMicroseconds(time_delta_us))));
}
@@ -1469,7 +1473,6 @@ bool QuicFramer::ProcessQuicCongestionFeedbackFrame(
}
case kTCP: {
CongestionFeedbackMessageTCP* tcp = &frame->tcp;
- // TODO(ianswett): Remove receive window, since it's constant.
uint16 receive_window = 0;
if (!reader_->ReadUInt16(&receive_window)) {
set_detailed_error("Unable to read receive window.");
@@ -1788,16 +1791,16 @@ size_t QuicFramer::ComputeFrameLength(
len += 1; // Congestion feedback type.
switch (congestion_feedback.type) {
- case kInterArrival: {
- const CongestionFeedbackMessageInterArrival& inter_arrival =
- congestion_feedback.inter_arrival;
+ case kTimestamp: {
+ const CongestionFeedbackMessageTimestamp& timestamp =
+ congestion_feedback.timestamp;
len += 1; // Number received packets.
- if (inter_arrival.received_packet_times.size() > 0) {
+ if (!timestamp.received_packet_times.empty()) {
len += PACKET_6BYTE_SEQUENCE_NUMBER; // Smallest received.
len += 8; // Time.
// 2 bytes per sequence number delta plus 4 bytes per delta time.
len += PACKET_6BYTE_SEQUENCE_NUMBER *
- (inter_arrival.received_packet_times.size() - 1);
+ (timestamp.received_packet_times.size() - 1);
}
break;
}
@@ -2094,55 +2097,8 @@ bool QuicFramer::AppendCongestionFeedbackFrame(
}
switch (frame.type) {
- case kInterArrival: {
- const CongestionFeedbackMessageInterArrival& inter_arrival =
- frame.inter_arrival;
- DCHECK_GE(numeric_limits<uint8>::max(),
- inter_arrival.received_packet_times.size());
- if (inter_arrival.received_packet_times.size() >
- numeric_limits<uint8>::max()) {
- return false;
- }
- // TODO(ianswett): Make num_received_packets a varint.
- uint8 num_received_packets =
- inter_arrival.received_packet_times.size();
- if (!writer->WriteBytes(&num_received_packets, 1)) {
- return false;
- }
- if (num_received_packets > 0) {
- TimeMap::const_iterator it =
- inter_arrival.received_packet_times.begin();
-
- QuicPacketSequenceNumber lowest_sequence = it->first;
- if (!AppendPacketSequenceNumber(PACKET_6BYTE_SEQUENCE_NUMBER,
- lowest_sequence, writer)) {
- return false;
- }
-
- QuicTime lowest_time = it->second;
- if (!writer->WriteUInt64(
- lowest_time.Subtract(creation_time_).ToMicroseconds())) {
- return false;
- }
-
- for (++it; it != inter_arrival.received_packet_times.end(); ++it) {
- QuicPacketSequenceNumber sequence_delta = it->first - lowest_sequence;
- DCHECK_GE(numeric_limits<uint16>::max(), sequence_delta);
- if (sequence_delta > numeric_limits<uint16>::max()) {
- return false;
- }
- if (!writer->WriteUInt16(static_cast<uint16>(sequence_delta))) {
- return false;
- }
-
- int32 time_delta_us =
- it->second.Subtract(lowest_time).ToMicroseconds();
- if (!writer->WriteBytes(&time_delta_us, sizeof(time_delta_us))) {
- return false;
- }
- }
- }
- break;
+ case kTimestamp: {
+ return AppendTimestampFrame(frame, writer);
}
case kTCP: {
const CongestionFeedbackMessageTCP& tcp = frame.tcp;
@@ -2161,6 +2117,53 @@ bool QuicFramer::AppendCongestionFeedbackFrame(
return true;
}
+bool QuicFramer::AppendTimestampFrame(
+ const QuicCongestionFeedbackFrame& frame,
+ QuicDataWriter* writer) {
+ const CongestionFeedbackMessageTimestamp& timestamp = frame.timestamp;
+ DCHECK_GE(numeric_limits<uint8>::max(),
+ timestamp.received_packet_times.size());
+ if (timestamp.received_packet_times.size() > numeric_limits<uint8>::max()) {
+ return false;
+ }
+ uint8 num_received_packets = timestamp.received_packet_times.size();
+ if (!writer->WriteBytes(&num_received_packets, 1)) {
+ return false;
+ }
+ if (num_received_packets > 0) {
+ TimeMap::const_iterator it = timestamp.received_packet_times.begin();
+
+ QuicPacketSequenceNumber lowest_sequence = it->first;
+ if (!AppendPacketSequenceNumber(PACKET_6BYTE_SEQUENCE_NUMBER,
+ lowest_sequence, writer)) {
+ return false;
+ }
+
+ QuicTime lowest_time = it->second;
+ if (!writer->WriteUInt64(
+ lowest_time.Subtract(creation_time_).ToMicroseconds())) {
+ return false;
+ }
+
+ for (++it; it != timestamp.received_packet_times.end(); ++it) {
+ QuicPacketSequenceNumber sequence_delta = it->first - lowest_sequence;
+ DCHECK_GE(numeric_limits<uint16>::max(), sequence_delta);
+ if (sequence_delta > numeric_limits<uint16>::max()) {
+ return false;
+ }
+ if (!writer->WriteUInt16(static_cast<uint16>(sequence_delta))) {
+ return false;
+ }
+
+ int32 time_delta_us = it->second.Subtract(lowest_time).ToMicroseconds();
+ if (!writer->WriteBytes(&time_delta_us, sizeof(time_delta_us))) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
bool QuicFramer::AppendStopWaitingFrame(
const QuicPacketHeader& header,
const QuicStopWaitingFrame& frame,
« no previous file with comments | « net/quic/quic_framer.h ('k') | net/quic/quic_framer_test.cc » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698