Experimental: rewrite extensions negotiation
This commit is contained in:
+123
-51
@@ -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};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user