123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971 |
- module vibe.http.internal.http2.server;
- import vibe.http.internal.http2.error;
- import vibe.http.internal.http2.multiplexing;
- import vibe.http.internal.http2.frame;
- import vibe.http.internal.http2.settings;
- import vibe.http.internal.http2.exchange;
- import vibe.http.internal.http2.hpack.tables;
- import vibe.http.internal.http2.hpack.hpack;
- import vibe.http.internal.http2.hpack.exception;
- import vibe.http.server;
- import vibe.container.internal.utilallocator;
- import vibe.core.log;
- import vibe.core.net;
- import vibe.core.core;
- import vibe.core.stream;
- import vibe.stream.tls;
- import vibe.internal.array;
- import vibe.internal.freelistref;
- import vibe.internal.interfaceproxy;
- import std.range;
- import std.base64;
- import std.traits;
- import std.bitmanip; // read from ubyte (decoding)
- import std.typecons;
- import std.conv : to;
- import std.exception : enforce;
- import std.algorithm : canFind; // alpn callback
- import std.algorithm.iteration;
- import std.variant : Algebraic;
- /*
- 3.2. Starting HTTP/2 for "http" URIs
- A client that makes a request for an "http" URI without prior
- knowledge about support for HTTP/2 on the next hop uses the HTTP
- Upgrade mechanism (Section 6.7 of [RFC7230]). The client does so by
- making an HTTP/1.1 request that includes an Upgrade header field with
- the "h2c" token. Such an HTTP/1.1 request MUST include exactly one
- HTTP2-Settings (Section 3.2.1) header field.
- For example:
- GET / HTTP/1.1
- Host: server.example.com
- Connection: Upgrade, HTTP2-Settings
- Upgrade: h2c
- HTTP2-Settings: <base64url encoding of HTTP/2 SETTINGS payload>
- Requests that contain a payload body MUST be sent in their entirety
- before the client can send HTTP/2 frames. This means that a large
- request can block the use of the connection until it is completely
- sent.
- If concurrency of an initial request with subsequent requests is
- important, an OPTIONS request can be used to perform the upgrade to
- HTTP/2, at the cost of an additional round trip.
- A server that does not support HTTP/2 can respond to the request as
- though the Upgrade header field were absent:
- HTTP/1.1 200 OK
- Content-Length: 243
- Content-Type: text/html
- ...
- A server MUST ignore an "h2" token in an Upgrade header field.
- Presence of a token with "h2" implies HTTP/2 over TLS, which is
- instead negotiated as described in Section 3.3.
- A server that supports HTTP/2 accepts the upgrade with a 101
- (Switching Protocols) response. After the empty line that terminates
- the 101 response, the server can begin sending HTTP/2 frames. These
- frames MUST include a response to the request that initiated the
- upgrade.
- For example:
- HTTP/1.1 101 Switching Protocols
- Connection: Upgrade
- Upgrade: h2c
- [ HTTP/2 connection ...
- The first HTTP/2 frame sent by the server MUST be a server connection
- preface (Section 3.5) consisting of a SETTINGS frame (Section 6.5).
- Upon receiving the 101 response, the client MUST send a connection
- preface (Section 3.5), which includes a SETTINGS frame.
- The HTTP/1.1 request that is sent prior to upgrade is assigned a
- stream identifier of 1 (see Section 5.1.1) with default priority
- values (Section 5.3.5). Stream 1 is implicitly "half-closed" from
- the client toward the server (see Section 5.1), since the request is
- completed as an HTTP/1.1 request. After commencing the HTTP/2
- connection, stream 1 is used for the response.
- 3.2.1. HTTP2-Settings Header Field
- A request that upgrades from HTTP/1.1 to HTTP/2 MUST include exactly
- one "HTTP2-Settings" header field. The HTTP2-Settings header field
- is a connection-specific header field that includes parameters that
- govern the HTTP/2 connection, provided in anticipation of the server
- accepting the request to upgrade.
- HTTP2-Settings = token68
- A server MUST NOT upgrade the connection to HTTP/2 if this header
- field is not present or if more than one is present. A server MUST
- NOT send this header field.
- The content of the HTTP2-Settings header field is the payload of a
- SETTINGS frame (Section 6.5), encoded as a base64url string (that is,
- the URL- and filename-safe Base64 encoding described in Section 5 of
- [RFC4648], with any trailing '=' characters omitted). The ABNF
- [RFC5234] production for "token68" is defined in Section 2.1 of
- [RFC7235].
- Since the upgrade is only intended to apply to the immediate
- connection, a client sending the HTTP2-Settings header field MUST
- also send "HTTP2-Settings" as a connection option in the Connection
- header field to prevent it from being forwarded (see Section 6.1 of
- [RFC7230]).
- A server decodes and interprets these values as it would any other
- SETTINGS frame. Explicit acknowledgement of these settings
- (Section 6.5.3) is not necessary, since a 101 response serves as
- implicit acknowledgement. Providing these values in the upgrade
- request gives a client an opportunity to provide parameters prior to
- receiving any frames from the server.
- */
- /**
- * an ALPN callback which can be used to detect the "h2" protocol
- * must be set before initializing the server with 'listenHTTP'
- * if the protocol is not set, it replies with HTTP/1.1
- */
- TLSALPNCallback http2Callback = (string[] choices) {
- if (choices.canFind("h2")) return "h2";
- else return "http/1.1";
- };
- private alias TLSStreamType = ReturnType!(createTLSStreamFL!(InterfaceProxy!Stream));
- /* ==================================================== */
- /* CONNECTION INITIALIZATION */
- /* ==================================================== */
- /** h2c protocol switching ONLY: Check if SETTINGS payload is valid by trying to decode it
- * if !valid, close connection and refuse to upgrade (RFC)
- * if valid, send SWITCHING_PROTOCOL response and start an HTTP/2 connection handler
- */
- bool startHTTP2Connection(ConnectionStream, H)(ConnectionStream connection, string h2settings,
- HTTP2ServerContext context, HTTPServerResponse switchRes, H headers, string st,
- IAllocator alloc, ubyte[] resBody) @safe
- if (isConnectionStream!ConnectionStream)
- {
- // init settings
- HTTP2Settings settings;
- logTrace("Starting HTTP/2 connection");
- // try decoding settings
- if (settings.decode!Base64URL(h2settings)) {
- context.settings = settings;
- // initialize IndexingTable (HPACK)
- () @trusted {
- if(!context.hasTable) context.table = FreeListRef!IndexingTable(context.settings.headerTableSize);
- // save response converted to HTTP/2
- context.resFrame = alloc.makeArray!ubyte(buildHeaderFrame!(StartLine.RESPONSE)
- (st, headers, context, alloc));
- context.resFrame ~= resBody;
- } ();
- // send response
- switchRes.switchToHTTP2(&handleHTTP2Connection!ConnectionStream, context);
- return true;
- } else {
- // reply with a 400 (bad request) header
- switchRes.sendBadRequest();
- connection.close;
- return false;
- }
- }
- /** client AND server should send a connection preface
- * server should receive a connection preface from the client + SETTINGS Frame
- * server connection preface consists of a SETTINGS Frame
- */
- void handleHTTP2Connection(ConnectionStream)(ConnectionStream stream,
- TCPConnection connection, HTTP2ServerContext context, bool priorKnowledge=false) @safe
- if (isConnectionStream!ConnectionStream || is(ConnectionStream : TLSStreamType))
- {
- logTrace("HTTP/2 Connection Handler");
- // read the connection preface
- if(!priorKnowledge) {
- ubyte[24] h2connPreface;
- stream.read(h2connPreface);
- if(h2connPreface != "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n") {
- logDebug("Ignoring invalid HTTP/2 client connection preface");
- return;
- }
- logTrace("Received client http2 connection preface");
- }
- // initialize Frame handler
- handleHTTP2FrameChain(stream, connection, context);
- }
- /* ==================================================== */
- /* FRAME HANDLING */
- /* ==================================================== */
- /// async frame handler: in charge of closing the connection if no data flows
- private void handleHTTP2FrameChain(ConnectionStream)(ConnectionStream stream, TCPConnection
- connection, HTTP2ServerContext context) @safe nothrow
- if (isConnectionStream!ConnectionStream || is(ConnectionStream : TLSStream))
- {
- logTrace("HTTP/2 Frame Chain Handler");
- static struct CB {
- ConnectionStream stream;
- TCPConnection connection;
- HTTP2ServerContext context;
- void opCall(bool st)
- {
- if (!st) connection.close;
- else runTask(&handleHTTP2FrameChain, stream, connection, context);
- }
- }
- while(true) {
- try {
- CB cb = {stream, connection, context};
- auto st = connection.waitForDataAsync(cb);
- final switch(st) {
- case WaitForDataAsyncStatus.waiting:
- logTrace("need to wait for more data asynchronously");
- return;
- case WaitForDataAsyncStatus.noMoreData:
- logTrace("connection closed by remote side");
- stream.finalize();
- connection.close();
- return;
- case WaitForDataAsyncStatus.dataAvailable:
- // start the frame handler
- bool close = handleHTTP2Frame(stream, connection, context);
- // determine if this connection needs to be closed
- if(close) {
- logTrace("Closing connection.");
- stream.finalize();
- connection.close();
- return;
- }
- }
- } catch (Exception e) {
- logException(e, "Failed to handle HTTP/2 frame chain");
- connection.close();
- return;
- }
- }
- }
- /// initializes an allocator and handles stream closing
- private bool handleHTTP2Frame(ConnectionStream)(ConnectionStream stream, TCPConnection
- connection, HTTP2ServerContext context) @trusted
- if (isConnectionStream!ConnectionStream || is(ConnectionStream : TLSStream))
- {
- import vibe.container.internal.utilallocator: RegionListAllocator;
- logTrace("HTTP/2 Frame Handler");
- bool close = false;
- () @trusted {
- version (VibeManualMemoryManagement)
- scope alloc = new RegionListAllocator!(shared(Mallocator), false)
- (1024, Mallocator.instance);
- else
- scope alloc = new RegionListAllocator!(shared(GCAllocator), true)
- (1024, GCAllocator.instance);
- if(!context.hasMultiplexer) context.multiplexer = FreeListRef!HTTP2Multiplexer(
- alloc,
- context.settings.maxConcurrentStreams,
- context.settings.initialWindowSize,
- context.settings.headerTableSize);
- if(!context.hasTable) context.table = FreeListRef!IndexingTable(context.settings.headerTableSize);
- // create a HTTP/2 Stream
- auto h2stream = HTTP2ConnectionStream!ConnectionStream(stream, 0, alloc);
- close = handleFrameAlloc(h2stream, connection, context, alloc);
- // if stream has to be closed
- if(h2stream.state == HTTP2StreamState.CLOSED) {
- try {
- closeStream(context.multiplexer, h2stream.streamId);
- } catch(Exception e) {
- logWarn("Unable to close stream: " ~ e.msg);
- close = true;
- }
- }
- } ();
- return close;
- }
- /// used (mixin) to check the validity of the received Frame w.r.to its stream ID
- private const string checkvalid = "enforceHTTP2(valid, \"Invalid stream ID\", HTTP2Error.STREAM_CLOSED);";
- /** Receives an HTTP2ConnectionStream, and handles the data received by decoding frames
- * Currently supports simple requests / responses
- * Stream Lifecycle is treated according to RFC 7540, Section 5.1
- */
- private bool handleFrameAlloc(ConnectionStream)(ref ConnectionStream stream, TCPConnection connection,
- HTTP2ServerContext context, IAllocator alloc) @trusted
- {
- logTrace("HTTP/2 Frame Handler (Alloc)");
- uint len = 0;
- // payload buffer
- auto rawBuf = AllocAppender!(ubyte[])(alloc);
- auto payload = AllocAppender!(ubyte[])(alloc);
- // Frame properties
- bool endStream = false;
- bool endHeaders = false;
- bool isAck = false;
- bool close = false;
- scope HTTP2FrameStreamDependency sdep;
- // frame struct
- scope HTTP2FrameHeader header;
- /* ==================================================== */
- /* read received header */
- /* ==================================================== */
- if(stream.canRead) {
- try {
- len = stream.readHeader(rawBuf);
- } catch (UncaughtException e) {
- // failed reading from stream, do not close the connection
- stream.state = HTTP2StreamState.CLOSED;
- return false;
- }
- } else {
- // failed reading from stream, do not close the connection
- //stream.state = HTTP2StreamState.CLOSED;
- return false;
- }
- // adjust buffer sizes
- rawBuf.reserve(len);
- payload.reserve(len);
- /* ==================================================== */
- /* read received payload */
- /* ==================================================== */
- if(len) stream.readPayload(rawBuf, len);
- /* ==================================================== */
- /* parse received Frame */
- /* ==================================================== */
- try {
- header = payload.unpackHTTP2Frame(rawBuf.data, endStream, endHeaders, isAck, sdep);
- } catch (HTTP2Exception e) {
- if (stream.state != HTTP2StreamState.IDLE || stream.streamId == 0) {
- ubyte[GOAWAYFrameLength] f;
- f.buildGOAWAYFrame(stream.streamId, e.code);
- stream.write(f);
- stream.state = HTTP2StreamState.CLOSED;
- logWarn("%s: %s", "Sent GOAWAY Frame", e.message);
- return true;
- } else {
- logDebug("Ignoring unsupported extension header.");
- return false;
- }
- } catch (Exception e) {
- logWarn(e.msg);
- }
- /* ==================================================== */
- /* register stream on MUX and determine Frame type */
- /* ==================================================== */
- try {
- auto valid = registerStream(context.multiplexer, header.streamId);
- logDebug("Received: "~to!string(header.type)~" on streamID "~to!string(header.streamId));
- enforceHTTP2(header.streamId % 2 != 0 || header.streamId == 0, "Clients cannot register even streams", HTTP2Error.PROTOCOL_ERROR);
- if(stream.needsContinuation) enforceHTTP2(header.type == HTTP2FrameType.CONTINUATION,
- "Expected continuation frame", HTTP2Error.PROTOCOL_ERROR);
- stream.streamId = header.streamId;
- final switch(header.type) {
- /* ==================================================== */
- /* DATA Frame (TODO) */
- /* ==================================================== */
- case HTTP2FrameType.DATA:
- mixin(checkvalid);
- if(endStream) {
- if(stream.state == HTTP2StreamState.HALF_CLOSED_LOCAL) {
- stream.state = HTTP2StreamState.CLOSED;
- closeStream(context.multiplexer, stream.streamId);
- } else if(stream.state == HTTP2StreamState.OPEN) {
- stream.state = HTTP2StreamState.HALF_CLOSED_REMOTE;
- } else if(stream.state == HTTP2StreamState.IDLE) {
- enforceHTTP2(false, "Invalid state", HTTP2Error.PROTOCOL_ERROR);
- } else {
- enforceHTTP2(false, "Stream closed", HTTP2Error.STREAM_CLOSED);
- }
- }
- break;
- /* ==================================================== */
- /* HEADERS Frame */
- /* ==================================================== */
- case HTTP2FrameType.HEADERS:
- mixin(checkvalid);
- enforceHTTP2(stream.streamId > 0, "Invalid stream ID", HTTP2Error.PROTOCOL_ERROR);
- stream.state = HTTP2StreamState.OPEN;
- if(sdep.isSet) {
- // update stream dependency with data in `sdep`
- }
- // save the header block for processing
- stream.putHeaderBlock(payload.data);
- if(endStream) {
- stream.state = HTTP2StreamState.HALF_CLOSED_REMOTE;
- }
- // parse headers in payload
- if(endHeaders) {
- logDebug("Received full HEADERS block");
- handleHTTP2HeadersFrame(stream, connection, context, alloc);
- } else {
- // wait for the next CONTINUATION frame until end_headers flag is set
- // END_STREAM flag does not count in this case
- logDebug("Incomplete HEADERS block, waiting for CONTINUATION frame.");
- close = handleFrameAlloc(stream, connection, context, alloc);
- }
- break;
- /* ==================================================== */
- /* PRIORITY Frame (TODO) */
- /* ==================================================== */
- case HTTP2FrameType.PRIORITY:
- // do not check validity since PRIORITY frames can be received on CLOSED
- // streams
- enforceHTTP2(stream.streamId > 0, "Invalid stream ID", HTTP2Error.PROTOCOL_ERROR);
- // update stream dependency with data in `sdep`
- break;
- /* ==================================================== */
- /* RST_STREAM Frame */
- /* ==================================================== */
- case HTTP2FrameType.RST_STREAM:
- enforceHTTP2(stream.state != HTTP2StreamState.IDLE || !valid,
- "Invalid state", HTTP2Error.PROTOCOL_ERROR);
- // reset stream in `closed` state
- if(stream.state != HTTP2StreamState.CLOSED) {
- closeStream(context.multiplexer, stream.streamId);
- }
- logDebug("RST_STREAM: Stream %d closed, error: %s",
- stream.streamId, cast(HTTP2Error)fromBytes(payload.data,4));
- break;
- /* ==================================================== */
- /* SETTINGS Frame */
- /* ==================================================== */
- case HTTP2FrameType.SETTINGS:
- if(!isAck) {
- handleHTTP2SettingsFrame(stream, connection, payload.data, header, context);
- } else {
- enforceHTTP2(payload.data.length == 0,
- "Invalid SETTINGS ACK (payload not empty)", HTTP2Error.FRAME_SIZE_ERROR);
- logDebug("Received SETTINGS ACK");
- }
- break;
- /* ==================================================== */
- /* PUSH_PROMISE Frame */
- /* ==================================================== */
- case HTTP2FrameType.PUSH_PROMISE:
- enforceHTTP2(false,
- "Client should not send PUSH_PROMISE Frames.", HTTP2Error.PROTOCOL_ERROR);
- break;
- /* ==================================================== */
- /* PING Frame */
- /* ==================================================== */
- case HTTP2FrameType.PING:
- if(!isAck) {
- // acknowledge ping with PING ACK Frame
- FixedAppender!(ubyte[], 17) buf;
- buf.createHTTP2FrameHeader(len, header.type, 0x1, header.streamId);
- // write PING Frame header
- stream.write(buf.data);
- // write PING Frame payload (equal to the received one)
- stream.write(payload.data);
- logDebug("Sent PING ACK response");
- }
- break;
- /* ==================================================== */
- /* GOAWAY Frame */
- /* ==================================================== */
- case HTTP2FrameType.GOAWAY:
- logDebug("Received GOAWAY Frame. Closing connection");
- stream.state = HTTP2StreamState.CLOSED;
- closeStream(context.multiplexer, stream.streamId);
- close = true;
- break;
- /* ==================================================== */
- /* WINDOW_UPDATE Frame */
- /* ==================================================== */
- case HTTP2FrameType.WINDOW_UPDATE:
- // can be received on closed streams (in case of pending data)
- enforceHTTP2(stream.state != HTTP2StreamState.IDLE || !valid ||
- stream.streamId == 0, "Invalid state", HTTP2Error.PROTOCOL_ERROR);
- auto inc = fromBytes(payload.data, 4);
- uint maxinc = 1 << 31;
- enforceHTTP2(inc > 0, "Invalid WINDOW_UPDATE increment", HTTP2Error.PROTOCOL_ERROR);
- // connection-based control window must be updated
- auto cw = connectionWindow(context.multiplexer);
- enforceHTTP2(cw + inc < maxinc, "Reached maximum WINDOW size",
- HTTP2Error.FLOW_CONTROL_ERROR);
- updateConnectionWindow(context.multiplexer, cw + inc);
- // per-stream control window must be updated (together with cw)
- if(stream.streamId > 0) {
- auto scw = streamConnectionWindow(context.multiplexer, stream.streamId);
- enforceHTTP2(scw + inc < maxinc, "Reached maximum WINDOW size",
- HTTP2Error.FLOW_CONTROL_ERROR);
- updateStreamConnectionWindow(context.multiplexer, stream.streamId, scw + inc);
- }
- // notify waiting DATA tasks if needed
- if(checkCondition(context.multiplexer, stream.streamId)) {
- logDebug("Notifying stopped tasks");
- notifyCondition(context.multiplexer);
- yield();
- }
- break;
- /* ==================================================== */
- /* CONTINUATION Frame
- /* ==================================================== */
- case HTTP2FrameType.CONTINUATION:
- // must be received immediately after a HEADERS Frame or a
- // CONTINUATION Frame
- enforceHTTP2(stream.state != HTTP2StreamState.IDLE, "Invalid state",
- HTTP2Error.PROTOCOL_ERROR);
- // add the received block to buffer
- stream.putHeaderBlock(payload.data);
- // process header block fragment in payload
- if(endHeaders) {
- logDebug("Received full HEADERS block");
- handleHTTP2HeadersFrame(stream, connection, context, alloc);
- } else {
- logDebug("Incomplete HEADERS block, waiting for CONTINUATION frame.");
- handleFrameAlloc(stream, connection, context, alloc);
- }
- break;
- }
- /* ==================================================== */
- /* `h2c`: First Response
- /* ==================================================== */
- static if(!is(ConnectionStream : TLSStream)) {
- if (context.resFrame) {
- auto l = context.resFrame.takeExactly(3).fromBytes(3) + 9;
- if(l < context.settings.maxFrameSize)
- {
- auto isEndStream = (context.resFrame.length > l) ? 0x0 : 0x1;
- context.resFrame[4] += 0x4 + isEndStream;
- try {
- stream.write(context.resFrame[0..l]);
- } catch (Exception e) {
- logWarn("Unable to write HEADERS Frame to stream");
- }
- } else {
- // TODO CONTINUATION frames
- assert(false);
- }
- auto resBody = context.resFrame[l..$];
- alloc.dispose(context.resFrame);
- // send DATA (body) if present
- // since the first response is part of HTTP/2 initialization,
- // this task is NOT executed asynchronously (for now) TODO
- if(resBody.length > 0) {
- auto dataFrame = AllocAppender!(ubyte[])(alloc);
- // create DATA Frame with END_STREAM (0x1) flag
- if(resBody.length > uint.max) assert(false, "TODO");
- // create DATA frame header
- dataFrame.createHTTP2FrameHeader(cast(uint)resBody.length, HTTP2FrameType.DATA, 0x1, 1);
- // append the DATA body
- dataFrame.put(resBody);
- // try writing data
- try {
- stream.write(dataFrame.data);
- } catch(Exception e) {
- logWarn("Unable to write DATA Frame to stream.");
- }
- logTrace("Sent DATA frame on streamID %s", stream.streamId);
- }
- }
- }
- closeStream(context.multiplexer, stream.streamId);
- } catch(HTTP2Exception e) {
- ubyte[GOAWAYFrameLength] f;
- f.buildGOAWAYFrame(stream.streamId, e.code);
- stream.write(f);
- logWarn("%s: %s", "Sent GOAWAY Frame", e.message);
- stream.state = HTTP2StreamState.CLOSED;
- return true;
- } catch(HPACKException e) {
- ubyte[GOAWAYFrameLength] f;
- f.buildGOAWAYFrame(stream.streamId, HTTP2Error.COMPRESSION_ERROR);
- stream.write(f);
- stream.state = HTTP2StreamState.CLOSED;
- logWarn("%s: %s", "Sent GOAWAY Frame", e.message);
- return true;
- } catch(Exception e) {
- logWarn(e.msg);
- }
- return close;
- }
- /// process an HEADERS frame
- void handleHTTP2HeadersFrame(Stream)(ref Stream stream, TCPConnection connection,
- HTTP2ServerContext context, IAllocator alloc)
- {
- // AllocAppender cannot be used here (TODO discuss)
- auto hdec = appender!(HTTP2HeaderTableField[])();
- // decode headers
- decodeHPACK(cast(immutable(ubyte)[])stream.headerBlock, hdec, context.table, alloc, context.settings.headerTableSize);
- // insert data in table
- hdec.data.each!((h) { if(h.index) context.table.insert(h); });
- // write a response (HEADERS + DATA according to request method)
- handleHTTP2Request(stream, connection, context, hdec.data, context.table, alloc);
- // clean the header block buffer
- stream.resetHeaderBlock();
- }
- /// handle SETTINGS frame exchange
- void handleHTTP2SettingsFrame(Stream)(ref Stream stream, TCPConnection connection, ubyte[] data, HTTP2FrameHeader header, HTTP2ServerContext context) @safe
- {
- // parse settings payload
- context.settings.unpackSettings(data);
- // update the connection window and notify waiting workers
- if(stream.streamId == 0) updateConnectionWindow(context.multiplexer, context.settings.initialWindowSize);
- updateStreamConnectionWindow(context.multiplexer, stream.streamId, context.settings.initialWindowSize);
- // notify waiting threads if needed
- if(checkCondition(context.multiplexer, stream.streamId)) {
- logTrace("Notifying stopped tasks");
- notifyCondition(context.multiplexer);
- //yield();
- }
- // acknowledge settings with SETTINGS ACK Frame
- FixedAppender!(ubyte[], 9) ackReply;
- ackReply.createHTTP2FrameHeader(0, header.type, 0x1, header.streamId);
- // new connection: must send a SETTINGS Frame as preface
- if(isConnectionPreface(context.multiplexer)) sendHTTP2SettingsFrame(stream, context);
- // write SETTINGS ACK
- stream.write(ackReply.data);
- logDebug("Sent SETTINGS ACK");
- }
- /// send a SETTINGS Frame
- void sendHTTP2SettingsFrame(Stream)(ref Stream stream, HTTP2ServerContext context) @safe
- {
- FixedAppender!(ubyte[], HTTP2HeaderLength+36) settingDst;
- settingDst.createHTTP2FrameHeader(36, HTTP2FrameType.SETTINGS, 0x0, 0);
- settingDst.serializeSettings(context.settings);
- stream.write(settingDst.data);
- logDebug("Sent SETTINGS Frame");
- }
- enum HTTP2StreamState {
- IDLE,
- RESERVED_LOCAL,
- RESERVED_REMOTE,
- OPEN,
- HALF_CLOSED_LOCAL,
- HALF_CLOSED_REMOTE,
- CLOSED
- }
- /** Represent a HTTP/2 Stream
- * The underlying connection can be TCPConnection or TLSStream
- * TODO: stream dependency, proper handling of stream IDs
- * approach: mantain a union of IDs so that only correct streams are initialized
- */
- struct HTTP2ConnectionStream(CS)
- {
- static assert(isConnectionStream!CS || is(CS : TLSStream) || isOutputStream!Stream);
- private {
- enum Parse { HEADER, PAYLOAD };
- CS m_conn;
- uint m_streamId;
- Parse toParse = Parse.HEADER;
- HTTP2StreamState m_state;
- AllocAppender!(ubyte[]) m_headerBlock;
- // Stream dependency TODO
- HTTP2FrameStreamDependency m_dependency;
- }
- // embed underlying connection
- alias m_conn this;
- this(CS)(ref CS conn, uint sid, IAllocator alloc) @safe
- {
- m_conn = conn;
- m_streamId = sid;
- m_state = HTTP2StreamState.IDLE;
- m_headerBlock = AllocAppender!(ubyte[])(alloc);
- }
- this(CS)(ref CS conn, IAllocator alloc) @safe
- {
- this(conn, 0, alloc);
- }
- @property CS connection() @safe { return m_conn; }
- @property HTTP2StreamState state() @safe @nogc { return m_state; }
- @property bool canRead() @safe @nogc
- {
- return (m_state == HTTP2StreamState.OPEN || m_state == HTTP2StreamState.IDLE);
- }
- /// set state according to Stream lifecycle (RFC 7540 section 5.1)
- @property void state(HTTP2StreamState st) @safe
- {
- switch(st) {
- // allowed: IDLE -> OPEN
- // OPEN -> OPEN
- case HTTP2StreamState.OPEN:
- if(m_state == HTTP2StreamState.IDLE ||
- m_state == HTTP2StreamState.OPEN)
- m_state = st;
- else enforceHTTP2(false, "Invalid state", HTTP2Error.PROTOCOL_ERROR);
- break;
- // allowed: OPEN -> HCLOCAL
- // RESERVED_REMOTE -> HCLOCAL
- // HCLOCAL -> HCLOCAL
- case HTTP2StreamState.HALF_CLOSED_LOCAL:
- if(m_state == HTTP2StreamState.OPEN ||
- m_state == HTTP2StreamState.RESERVED_REMOTE ||
- m_state == HTTP2StreamState.HALF_CLOSED_LOCAL)
- m_state = st;
- else enforceHTTP2(false, "Invalid state", HTTP2Error.PROTOCOL_ERROR);
- break;
- // allowed: OPEN -> HCREMOTE
- // RESERVED_LOCAL -> HCREMOTE
- // HCREMOTE -> HCREMOTE
- case HTTP2StreamState.HALF_CLOSED_REMOTE:
- if(m_state == HTTP2StreamState.OPEN ||
- m_state == HTTP2StreamState.RESERVED_LOCAL ||
- m_state == HTTP2StreamState.HALF_CLOSED_REMOTE)
- m_state = st;
- else enforceHTTP2(false, "Invalid state", HTTP2Error.PROTOCOL_ERROR);
- break;
- // allowed: all transitions to CLOSED
- // (RST_STREAM, GOAWAY permit this)
- case HTTP2StreamState.CLOSED:
- m_state = st;
- break;
- // specific to PUSH_PROMISE Frames
- case HTTP2StreamState.RESERVED_LOCAL:
- case HTTP2StreamState.RESERVED_REMOTE:
- if(m_state == HTTP2StreamState.IDLE) m_state = st;
- else enforceHTTP2(false, "Invalid state", HTTP2Error.PROTOCOL_ERROR);
- break;
- default:
- enforceHTTP2(false, "Invalid state", HTTP2Error.PROTOCOL_ERROR);
- }
- logTrace("Stream: %d state: %s", m_streamId, st);
- }
- @property uint streamId() @safe @nogc { return m_streamId; }
- @property void streamId(uint sid) @safe @nogc { m_streamId = sid; }
- @property HTTP2FrameStreamDependency dependency() @safe @nogc { return m_dependency; }
- @property ubyte[] headerBlock() @safe
- {
- assert(!m_headerBlock.data.empty, "No data in header block buffer");
- return m_headerBlock.data;
- }
- @property bool needsContinuation() @safe
- {
- return !m_headerBlock.data.empty;
- }
- /// reads from stream a frame header
- uint readHeader(R)(ref R dst) @safe
- {
- assert(toParse == Parse.HEADER);
- ubyte[HTTP2HeaderLength] buf; // should always be 9
- m_conn.read(buf);
- dst.put(buf);
- // length of payload
- auto len = dst.data[0..3].fromBytes(3);
- if(len > 0) toParse = Parse.PAYLOAD;
- return len;
- }
- /// reads from stream a frame payload
- void readPayload(R)(ref R dst, int len) @safe
- {
- assert(toParse == Parse.PAYLOAD);
- toParse = Parse.HEADER;
- ubyte[8] buf = void;
- /// perform multiple reads until payload is over (@nogc compatibility)
- while(len > 0) {
- auto end = (len < buf.length) ? len : buf.length;
- len -= m_conn.read(buf[0..end], IOMode.all);
- dst.put(buf[0..end]);
- }
- }
- /// save a HEADERS / CONTINUATION block for processing
- void putHeaderBlock(T)(T src) @safe
- if(isInputRange!T && is(ElementType!T : ubyte))
- {
- m_headerBlock.put(src);
- }
- void resetHeaderBlock() @trusted
- {
- m_headerBlock.reset(AppenderResetMode.freeData);
- }
- void finalize() @safe { }
- }
- unittest {
- import vibe.core.core : runApplication;
- // empty handler, just to test if protocol switching works
- void handleReq(scope HTTPServerRequest req, scope HTTPServerResponse res)
- @safe {
- if (req.path == "/")
- res.writeBody("Hello, World! This response is sent through HTTP/2");
- }
- auto settings = new HTTPServerSettings();
- settings.port = 8090;
- settings.bindAddresses = ["localhost"];
- listenHTTP(settings, &handleReq);
- //runApplication();
- }
- unittest {
- import vibe.core.core : runApplication;
- void handleRequest(scope HTTPServerRequest req, scope HTTPServerResponse res)
- @safe {
- if (req.path == "/")
- res.writeBody("Hello, World! This response is sent through HTTP/2\n");
- }
- auto settings = new HTTPServerSettings;
- settings.port = 8091;
- settings.bindAddresses = ["127.0.0.1", "192.168.1.131"];
- settings.tlsContext = createTLSContext(TLSContextKind.server);
- settings.tlsContext.useCertificateChainFile("tests/server.crt");
- settings.tlsContext.usePrivateKeyFile("tests/server.key");
- // set alpn callback to support HTTP/2
- // should accept the 'h2' protocol request
- settings.tlsContext.alpnCallback(http2Callback);
- // dummy, just for testing
- listenHTTP(settings, &handleRequest);
- //runApplication();
- }
|