123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471 |
- module vibe.http.internal.http2.frame;
- import vibe.http.internal.http2.settings;
- import vibe.http.internal.http2.error;
- import vibe.internal.array;
- import std.typecons;
- import std.traits;
- import std.range;
- import std.array;
- import std.exception;
- import std.algorithm.iteration;
- import std.algorithm.mutation;
- /** This module implements HTTP/2 Frames, as defined in RFC 7540 under:
- *
- * Section 4: Frame overview, Frame header composition (octets) and their meaning
- * https://tools.ietf.org/html/rfc7540#section-4
- *
- * Section 6: Frame definition according to Frame Type
- * https://tools.ietf.org/html/rfc7540#section-6
- */
- enum uint HTTP2HeaderLength = 9;
- enum HTTP2FrameType {
- DATA = 0x0,
- HEADERS = 0x1,
- PRIORITY = 0x2,
- RST_STREAM = 0x3,
- SETTINGS = 0x4,
- PUSH_PROMISE = 0x5,
- PING = 0x6,
- GOAWAY = 0x7,
- WINDOW_UPDATE = 0x8,
- CONTINUATION = 0x9
- }
- /*** FRAME PARSING ***/
- /// updated by `unpackHTTP2Frame`
- struct HTTP2FrameStreamDependency {
- bool exclusive = false;
- bool isPushPromise = false;
- uint streamId = 0;
- ubyte weight = 0;
- @property bool isSet() @safe @nogc { return streamId != 0; }
- void fill(R)(ref R src) @safe @nogc
- if(is(ElementType!R : ubyte))
- {
- uint first = src.takeExactly(4).fromBytes(4);
- exclusive = first & (cast(ulong)1 << 32);
- streamId = first & ((cast(ulong)1 << 32) - 1);
- src.popFrontN(4);
- if(!isPushPromise) {
- weight = src.front;
- src.popFront();
- }
- }
- }
- /** unpacks a frame putting the payload in `payloadDst` and returning the header
- * implements the checks required for each frame type (Section 6 of HTTP/2 RFC)
- *
- * Invoked by a possible HTTP/2 request handler, the payload is meant to be handled by
- * the caller.
- *
- * Note: @nogc-compatible as long as payloadDst.put is @nogc (AllocAppender.put isn't)
- */
- HTTP2FrameHeader unpackHTTP2Frame(R,T)(ref R payloadDst, T src, ref bool endStream, ref bool endHeaders, ref bool ack, ref HTTP2FrameStreamDependency sdep) @safe
- {
- auto header = unpackHTTP2FrameHeader(src);
- unpackHTTP2Frame(payloadDst, src, header, endStream, endHeaders, ack, sdep);
- return header;
- }
- /// DITTO
- void unpackHTTP2Frame(R,T)(ref R payloadDst, T src, HTTP2FrameHeader header, ref bool endStream, ref bool endHeaders, ref bool ack, ref HTTP2FrameStreamDependency sdep) @safe
- {
- size_t len = header.payloadLength;
- switch(header.type) {
- case HTTP2FrameType.DATA:
- if(header.flags & 0x8) { // padding is set, first bit is pad length
- len -= cast(size_t)src.front + 1;
- src.popFront();
- enforceHTTP2(src.length >= len, "Invalid pad length", HTTP2Error.PROTOCOL_ERROR);
- }
- foreach(b; src.takeExactly(len)) {
- payloadDst.put(b);
- src.popFront();
- }
- src.popFrontN(header.payloadLength - len - 1); // remove padding
- if(header.flags & 0x1) endStream = true;
- break;
- case HTTP2FrameType.HEADERS:
- if(header.flags & 0x8) { // padding is set, first bit is pad length
- len -= cast(size_t)src.front + 1;
- src.popFront();
- enforceHTTP2(src.length >= len, "Invalid pad length", HTTP2Error.PROTOCOL_ERROR);
- }
- if(header.flags & 0x20) { // priority is set, fill `sdep`
- sdep.fill(src);
- len -= 5;
- }
- foreach(b; src.takeExactly(len)) {
- payloadDst.put(b);
- src.popFront();
- }
- src.popFrontN(header.payloadLength - len - 1); // remove padding
- if(header.flags & 0x1) endStream = true;
- if(header.flags & 0x4) endHeaders = true;
- break;
- case HTTP2FrameType.PRIORITY:
- enforceHTTP2(len == 5, "Invalid PRIORITY Frame", HTTP2Error.PROTOCOL_ERROR);
- sdep.fill(src);
- break;
- case HTTP2FrameType.RST_STREAM:
- enforceHTTP2(len == 4, "Invalid RST_STREAM Frame", HTTP2Error.PROTOCOL_ERROR);
- foreach(b; src.takeExactly(len)) {
- payloadDst.put(b);
- src.popFront();
- }
- break;
- case HTTP2FrameType.SETTINGS:
- enforceHTTP2(len % 6 == 0, "Invalid SETTINGS Frame (FRAME_SIZE error)", HTTP2Error.PROTOCOL_ERROR);
- enforceHTTP2(header.streamId == 0, "Invalid streamId for SETTINGS Frame", HTTP2Error.PROTOCOL_ERROR);
- if(header.flags & 0x1) { // this is an ACK frame
- enforceHTTP2(len == 0, "Invalid SETTINGS ACK Frame (FRAME_SIZE error)", HTTP2Error.PROTOCOL_ERROR);
- ack = true;
- break;
- }
- foreach(b; src.takeExactly(len)) {
- payloadDst.put(b);
- src.popFront();
- }
- break;
- case HTTP2FrameType.PUSH_PROMISE:
- if(header.flags & 0x8) { // padding is set, first bit is pad length
- len -= cast(size_t)src.front + 1;
- src.popFront();
- enforceHTTP2(src.length >= len, "Invalid pad length", HTTP2Error.PROTOCOL_ERROR);
- }
- sdep.isPushPromise = true;
- sdep.fill(src);
- len -= 4;
- foreach(b; src.takeExactly(len)) {
- payloadDst.put(b);
- src.popFront();
- }
- src.popFrontN(header.payloadLength - len - 1); // remove padding
- if(header.flags & 0x4) endHeaders = true;
- break;
- case HTTP2FrameType.PING:
- enforceHTTP2(len == 8, "Invalid PING Frame (FRAME_SIZE error)",
- HTTP2Error.PROTOCOL_ERROR);
- enforceHTTP2(header.streamId == 0, "Invalid streamId for PING Frame",
- HTTP2Error.PROTOCOL_ERROR);
- if(header.flags & 0x1) {
- ack = true;
- }
- foreach(b; src.takeExactly(len)) {
- payloadDst.put(b);
- src.popFront();
- }
- break;
- case HTTP2FrameType.GOAWAY: // GOAWAY is used to close connection (in handler)
- enforceHTTP2(len >= 8, "Invalid GOAWAY Frame (FRAME_SIZE error)",
- HTTP2Error.PROTOCOL_ERROR);
- enforceHTTP2(header.streamId == 0, "Invalid streamId for GOAWAY Frame",
- HTTP2Error.PROTOCOL_ERROR);
- foreach(b; src.takeExactly(len)) {
- payloadDst.put(b);
- src.popFront();
- }
- break;
- case HTTP2FrameType.WINDOW_UPDATE:
- enforceHTTP2(len == 4, "Invalid WINDOW_UPDATE Frame (FRAME_SIZE error)",
- HTTP2Error.PROTOCOL_ERROR);
- foreach(i,b; src.takeExactly(len).enumerate) {
- if(i == 0) b &= 0x7F; // reserved bit
- payloadDst.put(b);
- src.popFront();
- }
- break;
- case HTTP2FrameType.CONTINUATION:
- enforceHTTP2(header.streamId != 0, "Invalid streamId for CONTINUATION frame",
- HTTP2Error.PROTOCOL_ERROR);
- foreach(b; src.takeExactly(len)) {
- payloadDst.put(b);
- src.popFront();
- }
- if(header.flags & 0x4) endHeaders = true;
- break;
- default:
- enforceHTTP2(false, "Invalid frame header unpacked.", HTTP2Error.PROTOCOL_ERROR);
- break;
- }
- }
- unittest {
- import vibe.internal.array : FixedAppender;
- FixedAppender!(ubyte[], 4) payloadDst;
- bool endStream = false;
- bool endHeaders = false;
- bool ack = false;
- HTTP2FrameStreamDependency sdep;
- // DATA Frame
- ubyte[] data = [0, 0, 4, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1];
- payloadDst.unpackHTTP2Frame(data, endStream, endHeaders, ack, sdep);
- assert(payloadDst.data == [1, 1, 1, 1]);
- // HEADERS Frame
- payloadDst.clear;
- data = [0, 0, 4, 1, 0, 0, 0, 0, 2, 2, 2, 2, 2];
- payloadDst.unpackHTTP2Frame(data, endStream, endHeaders, ack, sdep);
- assert(payloadDst.data == [2, 2, 2, 2]);
- // PRIORITY Frame
- payloadDst.clear;
- data = [0, 0, 5, 2, 0, 0, 0, 0, 3, 0, 0, 0, 2, 5];
- payloadDst.unpackHTTP2Frame(data, endStream, endHeaders, ack, sdep);
- assert(payloadDst.data == []);
- assert(sdep.weight == 5 && sdep.streamId == 2);
- // RST_STREAM Frame
- payloadDst.clear;
- data = [0, 0, 4, 3, 0, 0, 0, 0, 4, 4, 4, 4, 4];
- payloadDst.unpackHTTP2Frame(data, endStream, endHeaders, ack, sdep);
- assert(payloadDst.data == [4, 4, 4, 4]);
- // SETTINGS Frame
- FixedAppender!(ubyte[], 6) settingsDst;
- data = [0, 0, 6, 4, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 2];
- settingsDst.unpackHTTP2Frame(data, endStream, endHeaders, ack, sdep);
- assert(settingsDst.data == [0, 1, 2, 2, 2, 2]);
- // PUSH_PROMISE Frame
- payloadDst.clear;
- data = [0, 0, 8, 5, 0, 0, 0, 0, 5, 0, 0, 0, 2, 4, 4, 4, 4];
- payloadDst.unpackHTTP2Frame(data, endStream, endHeaders, ack, sdep);
- assert(payloadDst.data == [4, 4, 4, 4]);
- assert(sdep.weight == 5 && sdep.streamId == 2);
- // PING Frame
- FixedAppender!(ubyte[], 8) pingDst;
- data = [0, 0, 8, 6, 0, 0, 0, 0, 0, 0, 0, 0, 2, 4, 4, 4, 4];
- pingDst.unpackHTTP2Frame(data, endStream, endHeaders, ack, sdep);
- assert(pingDst.data == [0, 0, 0, 2, 4, 4, 4, 4]);
- // GOAWAY Frame
- pingDst.clear;
- data = [0, 0, 8, 7, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 4];
- pingDst.unpackHTTP2Frame(data, endStream, endHeaders, ack, sdep);
- assert(pingDst.data == [0, 0, 0, 2, 0, 0, 0, 4]);
- // WINDOW_UPDATE
- payloadDst.clear;
- data = [0, 0, 4, 8, 0, 0, 0, 0, 6, 1, 1, 1, 1];
- payloadDst.unpackHTTP2Frame(data, endStream, endHeaders, ack, sdep);
- assert(payloadDst.data == [1, 1, 1, 1]);
- // CONTINUATION
- payloadDst.clear;
- data = [0, 0, 4, 9, 0, 0, 0, 0, 6, 2, 2, 2, 2];
- payloadDst.unpackHTTP2Frame(data, endStream, endHeaders, ack, sdep);
- assert(payloadDst.data == [2, 2, 2, 2]);
- }
- /*** FRAME BUILDING ***/
- /// concatenates a Frame header with a Frame payload
- void buildHTTP2Frame(R,H,T)(ref R dst, ref H header, ref T payload) @safe @nogc
- if(is(ElementType!R : ubyte) && is(ElementType!T : ubyte))
- {
- // put header
- static if(is(H == HTTP2FrameHeader)) {
- assert(header.payloadLength == payload.length, "Invalid payload length");
- dst.serializeHTTP2FrameHeader(header);
- } else static if(is(ElementType!H : ubyte)) {
- auto len = header.takeExactly(3).fromBytes(3);
- assert(len == payload.length, "Invalid payload length");
- foreach(b; header) dst.put(b);
- }
- // put payload
- foreach(b; payload) dst.put(b);
- }
- /// DITTO
- /// @nogc-compatible if dst.put is @nogc
- void buildHTTP2Frame(R,T)(ref R dst, T payload) @safe
- {
- payload.copy(dst);
- }
- unittest {
- auto header = HTTP2FrameHeader(4, cast(HTTP2FrameType)1, 0, 5);
- ubyte[4] payload = [0, 1, 2, 3];
- ubyte[] bheader = [0, 0, 4, 1, 0, 0, 0, 0, 5];
- ubyte[13] expected = [0, 0, 4, 1, 0, 0, 0, 0, 5, 0, 1, 2, 3];
- BatchBuffer!(ubyte, 13) dst, ddst;
- dst.putN(13);
- ddst.putN(13);
- dst.buildHTTP2Frame(header, payload);
- ddst.buildHTTP2Frame(bheader, payload);
- assert(dst.peekDst == expected);
- assert(ddst.peekDst == expected);
- }
- /*** FRAME HEADER ***/
- /// header packing
- /// @nogc-compatible if dst.put is @nogc
- void createHTTP2FrameHeader(R)(ref R dst, const uint len, const HTTP2FrameType type, const ubyte flags, const uint sid) @safe
- {
- dst.serialize(HTTP2FrameHeader(len, type, flags, sid));
- }
- /// serializing
- void serializeHTTP2FrameHeader(R)(ref R dst, HTTP2FrameHeader header) @safe @nogc
- {
- dst.serialize(header);
- }
- /// unpacking
- HTTP2FrameHeader unpackHTTP2FrameHeader(R)(scope ref R src) @safe @nogc
- {
- scope header = HTTP2FrameHeader(src);
- return header;
- }
- /** Implement an HTTP/2 Frame header
- * The header is a 9-bit ubyte[9] string
- */
- struct HTTP2FrameHeader
- {
- private {
- //ubyte[3] m_length; // 24-bit frame payload length
- FixedAppender!(ubyte[], 3) m_length;
- HTTP2FrameType m_type; // frame type (stored as ubyte for serialization)
- ubyte m_flags; // frame flags
- //ubyte[4] m_streamId; // stream id, uint (stored as ubyte for serialization)
- FixedAppender!(ubyte[], 4) m_streamId;
- }
- this(const uint len, const HTTP2FrameType tp, const ubyte flg, const uint sid) @safe @nogc
- {
- assert(sid < (cast(ulong)1 << 32), "Invalid stream id");
- m_length.putBytes!3(len);
- m_type = tp;
- m_flags = flg;
- m_streamId.putBytes!4(sid & ((cast(ulong)1 << 32) - 1)); // reserved bit is 0
- }
- this(T)(ref T src) @safe @nogc
- if(is(ElementType!T : ubyte))
- {
- m_length.put(src.take(3));
- src.popFrontN(3);
- m_type = cast(HTTP2FrameType)src.front; src.popFront;
- m_flags = src.front; src.popFront;
- m_streamId.put(src.take(1).front & 127); src.popFront; // ignore reserved bit
- m_streamId.put(src.take(3));
- src.popFrontN(3);
- }
- @property HTTP2FrameType type() @safe @nogc { return m_type; }
- @property uint payloadLength() @safe @nogc { return m_length.data.fromBytes(3); }
- @property ubyte flags() @safe @nogc { return m_flags; }
- @property uint streamId() @safe @nogc { return m_streamId.data.fromBytes(4); }
- }
- /// convert 32-bit unsigned integer to N bytes (MSB first)
- void putBytes(uint N, R)(ref R dst, const(ulong) src) @safe @nogc
- {
- assert(src >= 0 && src < (cast(ulong)1 << N*8), "Invalid frame payload length");
- static if(hasLength!R) assert(dst.length >= N);
- ubyte[N] buf;
- foreach(i,ref b; buf) b = cast(ubyte)(src >> 8*(N-1-i)) & 0xff;
- static if(isArray!R) {
- dst.put(buf);
- } else {
- foreach(b; buf) dst.put(b);
- }
- }
- /// convert a N-bytes representation MSB->LSB to uint
- uint fromBytes(R)(R src, uint n) @safe @nogc
- {
- uint res = 0;
- static if(isArray!R) {
- foreach(i,b; src) res = res + (b << 8*(n-1-i));
- } else {
- foreach(i,b; src.enumerate.retro) res = res + (b << 8*i);
- }
- return res;
- }
- /// fill a buffer with fields from `header`
- /// @nogc-compatible if dst.put is @nogc
- private void serialize(R)(ref R dst, HTTP2FrameHeader header) @safe
- if(isOutputRange!(R, ubyte))
- {
- static foreach(f; __traits(allMembers, HTTP2FrameHeader)) {
- static if(f != "__ctor" && f != "type"
- && f != "payloadLength" && f != "flags" && f != "streamId") {
- static if(f == "m_length" || f == "m_streamId") {
- mixin("dst.put(header."~f~".data);");
- } else static if(f == "m_type") {
- mixin("dst.put(cast(ubyte)header."~f~");");
- } else {
- mixin("dst.put(header."~f~");");
- }
- }
- }
- }
- unittest {
- import vibe.internal.array : FixedAppender;
- auto header = HTTP2FrameHeader(2, cast(HTTP2FrameType)1, 0, 5);
- ubyte[] expected = [0, 0, 2, 1, 0, 0, 0, 0, 5];
- FixedAppender!(ubyte[], 9) dst;
- // serialize to a ubyte[9] array
- serialize(dst,header);
- assert(dst.data == expected);
- // test utility functions
- FixedAppender!(ubyte[], 9) ddst;
- ddst.createHTTP2FrameHeader(2, cast(HTTP2FrameType)1, 0, 5);
- assert(dst.data == ddst.data);
- FixedAppender!(ubyte[], 9) dddst;
- dddst.serializeHTTP2FrameHeader(header);
- assert(dst.data == dddst.data);
- // test unpacking
- assert(header == unpackHTTP2FrameHeader(expected));
- assert(header.payloadLength == 2);
- }
|