Experimental: rewrite extensions negotiation

This commit is contained in:
Alex Hultman
2021-01-11 07:27:40 +01:00
parent a86a500f3b
commit 7265ffe480
8 changed files with 236 additions and 155 deletions
+123 -51
View File
@@ -1,5 +1,5 @@
/*
* Authored by Alex Hultman, 2018-2020.
* Authored by Alex Hultman, 2018-2021.
* Intellectual property of third-party.
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,26 +18,32 @@
#ifndef UWS_WEBSOCKETEXTENSIONS_H
#define UWS_WEBSOCKETEXTENSIONS_H
/* There is a new, huge bug scenario that needs to be fixed:
* pub/sub does not support being in DEDICATED_COMPRESSOR-mode while having
* some clients downgraded to SHARED_COMPRESSOR - we cannot allow the client to
* demand a downgrade to SHARED_COMPRESSOR (yet) until we fix that scenario in pub/sub */
// #define UWS_ALLOW_SHARED_AND_DEDICATED_COMPRESSOR_MIX
#include <climits>
#include <cctype>
#include <string>
#include <string_view>
#include <tuple>
namespace uWS {
enum Options : unsigned int {
NO_OPTIONS = 0,
PERMESSAGE_DEFLATE = 1,
SERVER_NO_CONTEXT_TAKEOVER = 2, // remove this
CLIENT_NO_CONTEXT_TAKEOVER = 4, // remove this
NO_DELAY = 8,
SLIDING_DEFLATE_WINDOW = 16
};
enum ExtensionTokens {
/* Standard permessage-deflate tokens */
TOK_PERMESSAGE_DEFLATE = 1838,
TOK_SERVER_NO_CONTEXT_TAKEOVER = 2807,
TOK_CLIENT_NO_CONTEXT_TAKEOVER = 2783,
TOK_SERVER_MAX_WINDOW_BITS = 2372,
TOK_CLIENT_MAX_WINDOW_BITS = 2348
TOK_CLIENT_MAX_WINDOW_BITS = 2348,
/* Non-standard alias for Safari */
TOK_X_WEBKIT_DEFLATE_FRAME = 2149,
TOK_NO_CONTEXT_TAKEOVER = 2049,
TOK_MAX_WINDOW_BITS = 1614
};
struct ExtensionsParser {
@@ -45,12 +51,18 @@ private:
int *lastInteger = nullptr;
public:
/* Standard */
bool perMessageDeflate = false;
bool serverNoContextTakeover = false;
bool clientNoContextTakeover = false;
int serverMaxWindowBits = 0;
int clientMaxWindowBits = 0;
/* Non-standard Safari */
bool xWebKitDeflateFrame = false;
bool noContextTakeover = false;
int maxWindowBits = 0;
int getToken(const char *&in, const char *stop) {
while (in != stop && !isalnum(*in)) {
in++;
@@ -78,12 +90,28 @@ public:
ExtensionsParser(const char *data, size_t length) {
const char *stop = data + length;
int token = 1;
for (; token && token != TOK_PERMESSAGE_DEFLATE; token = getToken(data, stop));
/* Ignore anything before permessage-deflate or x-webkit-deflate-frame */
for (; token && token != TOK_PERMESSAGE_DEFLATE && token != TOK_X_WEBKIT_DEFLATE_FRAME; token = getToken(data, stop));
/* What protocol are we going to use? */
perMessageDeflate = (token == TOK_PERMESSAGE_DEFLATE);
xWebKitDeflateFrame = (token == TOK_X_WEBKIT_DEFLATE_FRAME);
while ((token = getToken(data, stop))) {
switch (token) {
case TOK_X_WEBKIT_DEFLATE_FRAME:
/* Duplicates not allowed/supported */
return;
case TOK_NO_CONTEXT_TAKEOVER:
noContextTakeover = true;
break;
case TOK_MAX_WINDOW_BITS:
maxWindowBits = 1;
lastInteger = &maxWindowBits;
break;
case TOK_PERMESSAGE_DEFLATE:
/* Duplicates not allowed/supported */
return;
case TOK_SERVER_NO_CONTEXT_TAKEOVER:
serverNoContextTakeover = true;
@@ -109,60 +137,104 @@ public:
}
};
template <bool isServer>
struct ExtensionsNegotiator {
protected:
unsigned int options;
/* Takes what we (the server) wants, returns what we got */
std::tuple<bool, int, int, std::string_view> negotiateCompression(bool wantCompression, int wantedCompressionWindow, int wantedInflationWindow, std::string_view offer) {
public:
ExtensionsNegotiator(unsigned int wantedOptions) {
options = wantedOptions;
/* If we don't want compression then we are done here */
if (!wantCompression) {
return {false, 0, 0, ""};
}
std::string generateOffer() {
std::string extensionsOffer;
if (options & Options::PERMESSAGE_DEFLATE) {
extensionsOffer += "permessage-deflate";
ExtensionsParser ep(offer.data(), offer.length());
if (options & Options::CLIENT_NO_CONTEXT_TAKEOVER) {
extensionsOffer += "; client_no_context_takeover";
static thread_local std::string response;
response = "";
int compressionWindow = wantedCompressionWindow;
int inflationWindow = wantedInflationWindow;
bool compression = false;
if (ep.xWebKitDeflateFrame) {
/* We now have compression */
compression = true;
response = "x-webkit-deflate-frame";
/* If the other peer has DEMANDED us no sliding window,
* we cannot compress with anything other than shared compressor */
if (ep.noContextTakeover) {
/* We must fail here right now (fix pub/sub) */
#ifndef UWS_ALLOW_SHARED_AND_DEDICATED_COMPRESSOR_MIX
if (wantedCompressionWindow != 0) {
return {false, 0, 0, ""};
}
#endif
/* It is questionable sending this improves anything */
/*if (options & Options::SERVER_NO_CONTEXT_TAKEOVER) {
extensionsOffer += "; server_no_context_takeover";
}*/
compressionWindow = 0;
}
return extensionsOffer;
}
/* If the other peer has DEMANDED us to use a limited sliding window,
* we have to limit out compression sliding window */
if (ep.maxWindowBits && ep.maxWindowBits < compressionWindow) {
compressionWindow = ep.maxWindowBits;
}
void readOffer(std::string_view offer) {
if (isServer) {
ExtensionsParser extensionsParser(offer.data(), offer.length());
if ((options & PERMESSAGE_DEFLATE) && extensionsParser.perMessageDeflate) {
if (extensionsParser.clientNoContextTakeover || (options & CLIENT_NO_CONTEXT_TAKEOVER)) {
options |= CLIENT_NO_CONTEXT_TAKEOVER;
}
/* We leave this option for us to read even if the client did not send it */
if (extensionsParser.serverNoContextTakeover) {
options |= SERVER_NO_CONTEXT_TAKEOVER;
}/* else {
options &= ~SERVER_NO_CONTEXT_TAKEOVER;
}*/
/* We decide our own inflation sliding window (and their compression sliding window) */
if (wantedInflationWindow < 15) {
if (!wantedInflationWindow) {
response += "; no_context_takeover";
} else {
options &= ~PERMESSAGE_DEFLATE;
response += "; max_window_bits=" + std::to_string(wantedInflationWindow);
}
}
} else if (ep.perMessageDeflate) {
/* We now have compression */
compression = true;
response = "permessage-deflate";
if (ep.clientNoContextTakeover) {
inflationWindow = 0;
} else if (ep.clientMaxWindowBits && ep.clientMaxWindowBits != 1) {
inflationWindow = std::min<int>(ep.clientMaxWindowBits, inflationWindow);
}
/* Whatever we have now, write */
if (inflationWindow < 15) {
if (!inflationWindow || !ep.clientMaxWindowBits) {
response += "; client_no_context_takeover";
inflationWindow = 0;
} else {
response += "; client_max_window_bits=" + std::to_string(inflationWindow);
}
}
/* This block basically lets the client lower it */
if (ep.serverNoContextTakeover) {
/* This is an important (temporary) fix since we haven't allowed
* these two modes to mix, and pub/sub will not handle this case (yet) */
#ifdef UWS_ALLOW_SHARED_AND_DEDICATED_COMPRESSOR_MIX
compressionWindow = 0;
#endif
} else if (ep.serverMaxWindowBits) {
compressionWindow = std::min<int>(ep.serverMaxWindowBits, compressionWindow);
}
/* Whatever we have now, write */
if (compressionWindow < 15) {
if (!compressionWindow) {
response += "; server_no_context_takeover";
} else {
response += "; server_max_window_bits=" + std::to_string(compressionWindow);
}
} else {
// todo!
}
}
unsigned int getNegotiatedOptions() {
return options;
/* A final sanity check (this check does not actually catch too high values!) */
if ((compressionWindow && compressionWindow < 8) || compressionWindow > 15 || (inflationWindow && inflationWindow < 8) || inflationWindow > 15) {
return {false, 0, 0, ""};
}
};
return {compression, compressionWindow, inflationWindow, response};
}
}