Hpack 阅读笔记

Hpack 阅读笔记

抽象

该规范定义了HPACK,HPACK的压缩格式 有效地表示要在HTTP / 2中使用的HTTP标头字段。

解决问题

该规范定义了HPACK,这是一种新型压缩机,它消除了冗余标头字段,将漏洞限制为已知安全性攻击,并且在受限条件下使用时具有有限的内存要求环境。

关键术语

标头字段:名称/值对。名称和值都是视为八位字节的不透明序列。 动态表:动态表是一个表,将存储的标头字段与索引值相关联。这个桌子是动态且特定于编码或解码上下文。 静态表:静态表是一个表,静态地将经常出现的标头字段与索引值。该表是有序的,只读的,始终可以访问,并且可以在所有编码或解码之间共享上下文。 标头列表:标头列表是标头字段的有序集合联合编码,并且可以包含重复的标头字段。HTTP / 2标头中包含的标头字段的完整列表块是标题列表。 标头字段表示形式:标头字段可以表示为编码形式,可以是文字形式或索引形式。 标头块:标头字段表示形式的有序列表,解码时会产生完整的标头列表。

索引表

HPACK使用两个表将标头字段与索引相关联。的静态表是预定义的,并且包含通用表标头字段(其中大多数带有空值)。动态表是动态的,编码器可以使用它来在编码的标题列表中重复的索引标题字段。 这两个表合并为一个地址空间,用于定义索引值。

索引地址空间

索引严格大于静态表的长度动态表中的元素的长度减去静态表以找到动态表中的索引。 索引严格大于两个表的长度之和必须视为解码错误。

头字段表示处理

定义了获取报头列表的头块的处理过程在这一部分。以确保解码成功生成一个报头列表时,解码器必须遵守以下规则。

头块中包含的所有头字段表示都是按照它们出现的顺序进行处理。

indexed表示形式需要执行以下操作: 对应于任一项中所引用条目的标题字段静态表或动态表被附加到解码后标头列表。 动态表中未添加的文字表示形式需要采取以下措施: 头字段被附加到解码后的标头列表中。 动态表的文字表示形式需要执行以下操作: 标头字段被附加到解码后的标头列表中。 头字段插入到动态内容的开头表。此插入可能导致驱逐先前的动态表中的条目。

计算表大小

动态表的大小是其表项大小的总和。 条目的大小是其名称长度的总和(以八位字节为单位),其值的长度(以八位字节为单位)和32。 条目的大小是使用其名称的长度和值,不应用任何霍夫曼编码。

动态表大小更改时的逐出

每当减小动态表的最大大小时,条目从动态表的末尾逐出,直到动态表小于或等于最大大小。

添加新条目时将条目逐出

在将新条目添加到动态表之前,逐出条目从动态表的末尾到动态表的大小小于或等于(最大大小-新条目大小)或直到表是空的。如果新条目的大小小于或等于最大大小,该条目将添加表中。这不是错误尝试添加一个大于最大大小的条目;一个尝试添加大于最大大小的条目导致表清空所有现有条目并生成一个空表。 新条目可以引用动态表中的条目名称将此新条目添加到动态广告素材中时将被逐出表。请注意实现,以免删除如果引用条目已从动态表中逐出,则引用名称表,然后插入新条目。

整数表示

整数用于表示名称索引,标头字段索引或字符串长度。整数表示可以从一个八位位组。为了优化处理,整数表示总是在八位字节的末尾结束。 整数分为两部分:前缀填充当前八位位组和可选的八位位组列表,如果整数值不适合前缀的位数前缀(称为N)是整数表示的参数。 如果整数值足够小,即严格小于2 ^ N-1,它被编码在N位前缀内。

否则,将前缀的所有位设置为1,并将值设置为使用一个或多个八位位组的列表对减少了2 ^ N-1的位进行编码。每个八位位组的最高有效位用作延续 标志:除列表中的最后一个八位字节外,其值均设置为1。八位位组的其余位用于编码减少的值。

image.jpeg
image.jpeg

代码示例:

image.jpeg
image.jpeg

字符串文字表示

头字段名称和头字段值可以表示为字符串文字。字符串文字被编码为直接编码字符串文字的八位字节或使用霍夫曼代码。

字符串文字表示形式包含以下字段: H:一位标志H,指示是否字符串是霍夫曼编码的。 字符串长度:用于编码字符串的八位字节数文字,编码为带有7位前缀的整数。 字符串数据:字符串文字的编码数据。如果H为0,那么编码后的数据就是字符串文字的原始八位位组。如果H为'1',则编码数据为Huffman编码字符串字面量。 使用霍夫曼编码的字符串文字使用霍夫曼代码。编码后的数据是对应于的每个八位位组的代码的按位串联字符串文字。 由于霍夫曼编码的数据并不总是以八位字节为边界,在其后插入一些填充,直到下一个八位位组边界。至防止将此填充误解为字符串的一部分文字,代码的最高有效位对应于使用EOS(字符串结尾)符号。 解码时,编码数据末尾的不完整代码为被视为填充和丢弃。填充严格更长超过7位必须视为解码错误。填充不对应于EOS代码的最高有效位 符号必须被视为解码错误。霍夫曼编码的字符串包含EOS符号的文字必须被视为解码错误。

文字标题字段表示

文字标头字段表示形式包含文字标头字段值。标头字段名称以文字形式或通过以下方式提供从静态表或现有表中引用现有表项动态表。 本规范定义了文字头字段的三种形式表示形式:有索引,无索引,从不索引。

带增量索引的文字标题字段

具有增量索引表示的文字标头字段结果是将头字段附加到解码的头列表中,并且将其作为新条目插入动态表。

image.jpeg
image.jpeg

具有增量索引表示的文字标头字段从“ 01” 2位模式开始。

没有索引的文字标题字段

没有索引表示的文字标头字段会导致将标题字段附加到已解码的标题列表,而无需更改动态表。

image.jpeg
image.jpeg

没有索引表示形式的文字标头字段以'0000'4位模式。

文字头字段从不索引

文字标头字段永不索引表示形式导致将标题字段附加到已解码的标题列表,而无需更改动态表。

image.jpeg
image.jpeg

文字标头字段永不索引的表示形式以'0001'4位模式。

动态表大小更新

动态表格大小更新从“ 001” 3位模式开始,然后是新的最大尺寸,以整数表示,带有5位前缀。 新的最大尺寸必须小于或等于限制由协议使用HPACK确定。超过此值限制必须视为解码错误。在HTTP / 2中,此限制为SETTINGS_HEADER_TABLE_SIZE参数的最后一个值(请参见从解码器收到并确认的通过编码器。 减小动态表的最大大小可能会导致条目被驱逐。

image.jpeg

适用于HPACK和HTTP

HPACK可以缓解但不能完全阻止仿照通过强制猜测以匹配整个标头字段来实现CRIME [ CRIME ]值而不是单个字符。攻击者只能学习猜测是否正确,因此将其简化为蛮力 猜测标题字段的值。 因此,恢复特定标头字段值的可行性取决于值的熵。结果,具有高价值的不太可能成功恢复。但是,价值低的参数仍然脆弱。 这种性质的攻击有可能在任何两个相互之间的时间不信任的实体控制放置的请求或响应到单个HTTP / 2连接上。如果共享HPACK压缩器允许一个实体向动态表中添加条目,而另一个实体访问这些条目,则可以了解表的状态。 来自互不信任实体的请求或响应发生以下情况时,中介者: 在单个连接上将来自多个客户端的请求发送到原始服务器,或从多个原始服务器获取响应并将其放置在与客户端的共享连接。 Web浏览器还需要假设请求是在相同的不同Web来源的连接是相互建立的不信任的实体。

twitter hpack

关键代码:

Encoder.java


/**

* Encode the header field into the header block.

*/

public void encodeHeader(OutputStream out, byte[] name, byte[] value, boolean sensitive) throws IOException {

// If the header value is sensitive then it must never be indexed

if (sensitive) {

int nameIndex = getNameIndex(name);

encodeLiteral(out, name, value, IndexType.NEVER, nameIndex);

return;

}

// If the peer will only use the static table

if (capacity == 0) {

int staticTableIndex = StaticTable.getIndex(name, value);

if (staticTableIndex == -1) {

int nameIndex = StaticTable.getIndex(name);

encodeLiteral(out, name, value, IndexType.NONE, nameIndex);

} else {

encodeInteger(out, 0x80, 7, staticTableIndex);

}

return;

}

int headerSize = HeaderField.sizeOf(name, value);

// If the headerSize is greater than the max table size then it must be encoded literally

if (headerSize > capacity) {

int nameIndex = getNameIndex(name);

encodeLiteral(out, name, value, IndexType.NONE, nameIndex);

return;

}

HeaderEntry headerField = getEntry(name, value);

if (headerField != null) {

int index = getIndex(headerField.index) + StaticTable.length;

// Section 6.1\. Indexed Header Field Representation

encodeInteger(out, 0x80, 7, index);

} else {

int staticTableIndex = StaticTable.getIndex(name, value);

if (staticTableIndex != -1) {

// Section 6.1\. Indexed Header Field Representation

encodeInteger(out, 0x80, 7, staticTableIndex);

} else {

int nameIndex = getNameIndex(name);

if (useIndexing) {

ensureCapacity(headerSize);

}

IndexType indexType = useIndexing ? IndexType.INCREMENTAL : IndexType.NONE;

encodeLiteral(out, name, value, indexType, nameIndex);

if (useIndexing) {

add(name, value);

}

}

}

}

Decode.java


/**

* Decode the header block into header fields.

*/

public void decode(InputStream in, HeaderListener headerListener) throws IOException {

while (in.available() > 0) {

switch (state) {

case READ_HEADER_REPRESENTATION:

byte b = (byte) in.read();

if (maxDynamicTableSizeChangeRequired && (b & 0xE0) != 0x20) {

// Encoder MUST signal maximum dynamic table size change

throw MAX_DYNAMIC_TABLE_SIZE_CHANGE_REQUIRED;

}

if (b < 0) {

// Indexed Header Field

index = b & 0x7F;

if (index == 0) {

throw ILLEGAL_INDEX_VALUE;

} else if (index == 0x7F) {

state = State.READ_INDEXED_HEADER;

} else {

indexHeader(index, headerListener);

}

} else if ((b & 0x40) == 0x40) {

// Literal Header Field with Incremental Indexing

indexType = IndexType.INCREMENTAL;

index = b & 0x3F;

if (index == 0) {

state = State.READ_LITERAL_HEADER_NAME_LENGTH_PREFIX;

} else if (index == 0x3F) {

state = State.READ_INDEXED_HEADER_NAME;

} else {

// Index was stored as the prefix

readName(index);

state = State.READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX;

}

} else if ((b & 0x20) == 0x20) {

// Dynamic Table Size Update

index = b & 0x1F;

if (index == 0x1F) {

state = State.READ_MAX_DYNAMIC_TABLE_SIZE;

} else {

setDynamicTableSize(index);

state = State.READ_HEADER_REPRESENTATION;

}

} else {

// Literal Header Field without Indexing / never Indexed

indexType = ((b & 0x10) == 0x10) ? IndexType.NEVER : IndexType.NONE;

index = b & 0x0F;

if (index == 0) {

state = State.READ_LITERAL_HEADER_NAME_LENGTH_PREFIX;

} else if (index == 0x0F) {

state = State.READ_INDEXED_HEADER_NAME;

} else {

// Index was stored as the prefix

readName(index);

state = State.READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX;

}

}

break;

case READ_MAX_DYNAMIC_TABLE_SIZE:

int maxSize = decodeULE128(in);

if (maxSize == -1) {

return;

}

// Check for numerical overflow

if (maxSize > Integer.MAX_VALUE - index) {

throw DECOMPRESSION_EXCEPTION;

}

setDynamicTableSize(index + maxSize);

state = State.READ_HEADER_REPRESENTATION;

break;

case READ_INDEXED_HEADER:

int headerIndex = decodeULE128(in);

if (headerIndex == -1) {

return;

}

// Check for numerical overflow

if (headerIndex > Integer.MAX_VALUE - index) {

throw DECOMPRESSION_EXCEPTION;

}

indexHeader(index + headerIndex, headerListener);

state = State.READ_HEADER_REPRESENTATION;

break;

case READ_INDEXED_HEADER_NAME:

// Header Name matches an entry in the Header Table

int nameIndex = decodeULE128(in);

if (nameIndex == -1) {

return;

}

// Check for numerical overflow

if (nameIndex > Integer.MAX_VALUE - index) {

throw DECOMPRESSION_EXCEPTION;

}

readName(index + nameIndex);

state = State.READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX;

break;

case READ_LITERAL_HEADER_NAME_LENGTH_PREFIX:

b = (byte) in.read();

huffmanEncoded = (b & 0x80) == 0x80;

index = b & 0x7F;

if (index == 0x7f) {

state = State.READ_LITERAL_HEADER_NAME_LENGTH;

} else {

nameLength = index;

// Disallow empty names -- they cannot be represented in HTTP/1.x

if (nameLength == 0) {

throw DECOMPRESSION_EXCEPTION;

}

// Check name length against max header size

if (exceedsMaxHeaderSize(nameLength)) {

if (indexType == IndexType.NONE) {

// Name is unused so skip bytes

name = EMPTY;

skipLength = nameLength;

state = State.SKIP_LITERAL_HEADER_NAME;

break;

}

// Check name length against max dynamic table size

if (nameLength + HEADER_ENTRY_OVERHEAD > dynamicTable.capacity()) {

dynamicTable.clear();

name = EMPTY;

skipLength = nameLength;

state = State.SKIP_LITERAL_HEADER_NAME;

break;

}

}

state = State.READ_LITERAL_HEADER_NAME;

}

break;

case READ_LITERAL_HEADER_NAME_LENGTH:

// Header Name is a Literal String

nameLength = decodeULE128(in);

if (nameLength == -1) {

return;

}

// Check for numerical overflow

if (nameLength > Integer.MAX_VALUE - index) {

throw DECOMPRESSION_EXCEPTION;

}

nameLength += index;

// Check name length against max header size

if (exceedsMaxHeaderSize(nameLength)) {

if (indexType == IndexType.NONE) {

// Name is unused so skip bytes

name = EMPTY;

skipLength = nameLength;

state = State.SKIP_LITERAL_HEADER_NAME;

break;

}

// Check name length against max dynamic table size

if (nameLength + HEADER_ENTRY_OVERHEAD > dynamicTable.capacity()) {

dynamicTable.clear();

name = EMPTY;

skipLength = nameLength;

state = State.SKIP_LITERAL_HEADER_NAME;

break;

}

}

state = State.READ_LITERAL_HEADER_NAME;

break;

case READ_LITERAL_HEADER_NAME:

// Wait until entire name is readable

if (in.available() < nameLength) {

return;

}

name = readStringLiteral(in, nameLength);

state = State.READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX;

break;

case SKIP_LITERAL_HEADER_NAME:

skipLength -= in.skip(skipLength);

if (skipLength == 0) {

state = State.READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX;

}

break;

case READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX:

b = (byte) in.read();

huffmanEncoded = (b & 0x80) == 0x80;

index = b & 0x7F;

if (index == 0x7f) {

state = State.READ_LITERAL_HEADER_VALUE_LENGTH;

} else {

valueLength = index;

// Check new header size against max header size

long newHeaderSize = (long) nameLength + (long) valueLength;

if (exceedsMaxHeaderSize(newHeaderSize)) {

// truncation will be reported during endHeaderBlock

headerSize = maxHeaderSize + 1;

if (indexType == IndexType.NONE) {

// Value is unused so skip bytes

state = State.SKIP_LITERAL_HEADER_VALUE;

break;

}

// Check new header size against max dynamic table size

if (newHeaderSize + HEADER_ENTRY_OVERHEAD > dynamicTable.capacity()) {

dynamicTable.clear();

state = State.SKIP_LITERAL_HEADER_VALUE;

break;

}

}

if (valueLength == 0) {

insertHeader(headerListener, name, EMPTY, indexType);

state = State.READ_HEADER_REPRESENTATION;

} else {

state = State.READ_LITERAL_HEADER_VALUE;

}

}

break;

case READ_LITERAL_HEADER_VALUE_LENGTH:

// Header Value is a Literal String

valueLength = decodeULE128(in);

if (valueLength == -1) {

return;

}

// Check for numerical overflow

if (valueLength > Integer.MAX_VALUE - index) {

throw DECOMPRESSION_EXCEPTION;

}

valueLength += index;

// Check new header size against max header size

long newHeaderSize = (long) nameLength + (long) valueLength;

if (newHeaderSize + headerSize > maxHeaderSize) {

// truncation will be reported during endHeaderBlock

headerSize = maxHeaderSize + 1;

if (indexType == IndexType.NONE) {

// Value is unused so skip bytes

state = State.SKIP_LITERAL_HEADER_VALUE;

break;

}

// Check new header size against max dynamic table size

if (newHeaderSize + HEADER_ENTRY_OVERHEAD > dynamicTable.capacity()) {

dynamicTable.clear();

state = State.SKIP_LITERAL_HEADER_VALUE;

break;

}

}

state = State.READ_LITERAL_HEADER_VALUE;

break;

case READ_LITERAL_HEADER_VALUE:

// Wait until entire value is readable

if (in.available() < valueLength) {

return;

}

byte[] value = readStringLiteral(in, valueLength);

insertHeader(headerListener, name, value, indexType);

state = State.READ_HEADER_REPRESENTATION;

break;

case SKIP_LITERAL_HEADER_VALUE:

valueLength -= in.skip(valueLength);

if (valueLength == 0) {

state = State.READ_HEADER_REPRESENTATION;

}

break;

default:

throw new IllegalStateException("should not reach here");

}

}

}

HuffmanEncoder.java


/**

* Compresses the input string literal using the Huffman coding.

*

* @param out the output stream for the compressed data

* @param data the string literal to be Huffman encoded

* @param off the start offset in the data

* @param len the number of bytes to encode

* @throws IOException if an I/O error occurs. In particular,

* an <code>IOException</code> may be thrown if the

* output stream has been closed.

*/

public void encode(OutputStream out, byte[] data, int off, int len) throws IOException {

if (out == null) {

throw new NullPointerException("out");

} else if (data == null) {

throw new NullPointerException("data");

} else if (off < 0 || len < 0 || (off + len) < 0 || off > data.length || (off + len) > data.length) {

throw new IndexOutOfBoundsException();

} else if (len == 0) {

return;

}

long current = 0;

int n = 0;

for (int i = 0; i < len; i++) {

int b = data[off + i] & 0xFF;

int code = codes[b];

int nbits = lengths[b];

current <<= nbits;

current |= code;

n += nbits;

while (n >= 8) {

n -= 8;

out.write(((int) (current >> n)));

}

}

if (n > 0) {

current <<= (8 - n);

current |= (0xFF >>> n); // this should be EOS symbol

out.write((int) current);

}

}


/**

* Returns the number of bytes required to Huffman encode the input string literal.

*

* @param data the string literal to be Huffman encoded

* @return the number of bytes required to Huffman encode <code>data</code>

*/

public int getEncodedLength(byte[] data) {

if (data == null) {

throw new NullPointerException("data");

}

long len = 0;

for (byte b : data) {

len += lengths[b & 0xFF];

}

return (int) ((len + 7) >> 3);

}

HuffmanDecoder.java


/**

* Decompresses the given Huffman coded string literal.

*

* @param buf the string literal to be decoded

* @return the output stream for the compressed data

* @throws IOException if an I/O error occurs. In particular,

* an <code>IOException</code> may be thrown if the

* output stream has been closed.

*/

public byte[] decode(byte[] buf) throws IOException {

ByteArrayOutputStream baos = new ByteArrayOutputStream();

Node node = root;

int current = 0;

int bits = 0;

for (int i = 0; i < buf.length; i++) {

int b = buf[i] & 0xFF;

current = (current << 8) | b;

bits += 8;

while (bits >= 8) {

int c = (current >>> (bits - 8)) & 0xFF;

node = node.children[c];

bits -= node.bits;

if (node.isTerminal()) {

if (node.symbol == HpackUtil.HUFFMAN_EOS) {

throw EOS_DECODED;

}

baos.write(node.symbol);

node = root;

}

}

}

while (bits > 0) {

int c = (current << (8 - bits)) & 0xFF;

node = node.children[c];

if (node.isTerminal() && node.bits <= bits) {

bits -= node.bits;

baos.write(node.symbol);

node = root;

} else {

break;

}

}

// Section 5.2\. String Literal Representation

// Padding not corresponding to the most significant bits of the code

// for the EOS symbol (0xFF) MUST be treated as a decoding error.

int mask = (1 << bits) - 1;

if ((current & mask) != mask) {

throw INVALID_PADDING;

}

return baos.toByteArray();

}


private static void insert(Node root, int symbol, int code, byte length) {

// traverse tree using the most significant bytes of code

Node current = root;

while (length > 8) {

if (current.isTerminal()) {

throw new IllegalStateException("invalid Huffman code: prefix not unique");

}

length -= 8;

int i = (code >>> length) & 0xFF;

if (current.children[i] == null) {

current.children[i] = new Node();

}

current = current.children[I];

}

Node terminal = new Node(symbol, length);

int shift = 8 - length;

int start = (code << shift) & 0xFF;

int end = 1 << shift;

for (int i = start; i < start + end; i++) {

current.children[i] = terminal;

}

}

附录

静态表定义

静态表包含一个预定义的标头字段的固定列表。 静态表是根据最常见的标题字段创建的由流行的网站使用,另外还有特定于HTTP / 2的信息伪头字段。对于标题具有一些频繁值的字段,为每个字段添加了一个条目这些频繁的价值观对于其他标题字段,添加了一个条目具有空值。 表列出了构成静态对象的预定义头字段表并给出每个条目的索引。

image.jpeg
image.jpeg

Huffman Code

表中的每一行均定义用于表示符号的代码: sym:要表示的符号。它是一个十进制值八位位组,可能以ASCII表示形式开头。一个特定符号“ EOS”用于指示字符串的结尾文字。 位编码:符号的霍夫曼编码,表示为以2为基的整数,在最高有效位(MSB)上对齐。 十六进制代码:符号的霍夫曼代码,表示为十六进制整数,在最低有效位(LSB)上对齐。 len:代表符号的代码的位数。

image.jpeg
image.jpeg
image.jpeg
image.jpeg
image.jpeg
image.jpeg
image.jpeg
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341