/src/uWebSockets/src/WebSocketExtensions.h
Line | Count | Source (jump to first uncovered line) |
1 | | /* |
2 | | * Authored by Alex Hultman, 2018-2021. |
3 | | * Intellectual property of third-party. |
4 | | |
5 | | * Licensed under the Apache License, Version 2.0 (the "License"); |
6 | | * you may not use this file except in compliance with the License. |
7 | | * You may obtain a copy of the License at |
8 | | |
9 | | * http://www.apache.org/licenses/LICENSE-2.0 |
10 | | |
11 | | * Unless required by applicable law or agreed to in writing, software |
12 | | * distributed under the License is distributed on an "AS IS" BASIS, |
13 | | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
14 | | * See the License for the specific language governing permissions and |
15 | | * limitations under the License. |
16 | | */ |
17 | | |
18 | | #ifndef UWS_WEBSOCKETEXTENSIONS_H |
19 | | #define UWS_WEBSOCKETEXTENSIONS_H |
20 | | |
21 | | /* There is a new, huge bug scenario that needs to be fixed: |
22 | | * pub/sub does not support being in DEDICATED_COMPRESSOR-mode while having |
23 | | * some clients downgraded to SHARED_COMPRESSOR - we cannot allow the client to |
24 | | * demand a downgrade to SHARED_COMPRESSOR (yet) until we fix that scenario in pub/sub */ |
25 | | // #define UWS_ALLOW_SHARED_AND_DEDICATED_COMPRESSOR_MIX |
26 | | |
27 | | /* We forbid negotiating 8 windowBits since Zlib has a bug with this */ |
28 | | // #define UWS_ALLOW_8_WINDOW_BITS |
29 | | |
30 | | #include <climits> |
31 | | #include <cctype> |
32 | | #include <string> |
33 | | #include <string_view> |
34 | | #include <tuple> |
35 | | |
36 | | namespace uWS { |
37 | | |
38 | | enum ExtensionTokens { |
39 | | /* Standard permessage-deflate tokens */ |
40 | | TOK_PERMESSAGE_DEFLATE = 1838, |
41 | | TOK_SERVER_NO_CONTEXT_TAKEOVER = 2807, |
42 | | TOK_CLIENT_NO_CONTEXT_TAKEOVER = 2783, |
43 | | TOK_SERVER_MAX_WINDOW_BITS = 2372, |
44 | | TOK_CLIENT_MAX_WINDOW_BITS = 2348, |
45 | | /* Non-standard alias for Safari */ |
46 | | TOK_X_WEBKIT_DEFLATE_FRAME = 2149, |
47 | | TOK_NO_CONTEXT_TAKEOVER = 2049, |
48 | | TOK_MAX_WINDOW_BITS = 1614 |
49 | | |
50 | | }; |
51 | | |
52 | | struct ExtensionsParser { |
53 | | private: |
54 | | int *lastInteger = nullptr; |
55 | | |
56 | | public: |
57 | | /* Standard */ |
58 | | bool perMessageDeflate = false; |
59 | | bool serverNoContextTakeover = false; |
60 | | bool clientNoContextTakeover = false; |
61 | | int serverMaxWindowBits = 0; |
62 | | int clientMaxWindowBits = 0; |
63 | | |
64 | | /* Non-standard Safari */ |
65 | | bool xWebKitDeflateFrame = false; |
66 | | bool noContextTakeover = false; |
67 | | int maxWindowBits = 0; |
68 | | |
69 | 6.27k | int getToken(const char *&in, const char *stop) { |
70 | 9.97k | while (in != stop && !isalnum(*in)) { |
71 | 3.69k | in++; |
72 | 3.69k | } |
73 | | |
74 | | /* Don't care more than this for now */ |
75 | 6.27k | static_assert(SHRT_MIN > INT_MIN, "Integer overflow fix is invalid for this platform, report this as a bug!"); |
76 | | |
77 | 6.27k | int hashedToken = 0; |
78 | 4.31M | while (in != stop && (isalnum(*in) || *in == '-' || *in == '_')) { |
79 | 4.31M | if (isdigit(*in)) { |
80 | | /* This check is a quick and incorrect fix for integer overflow |
81 | | * in oss-fuzz but we don't care as it doesn't matter either way */ |
82 | 6.94k | if (hashedToken > SHRT_MIN && hashedToken < SHRT_MAX) { |
83 | 5.96k | hashedToken = hashedToken * 10 - (*in - '0'); |
84 | 5.96k | } |
85 | 4.30M | } else { |
86 | 4.30M | hashedToken += *in; |
87 | 4.30M | } |
88 | 4.31M | in++; |
89 | 4.31M | } |
90 | 6.27k | return hashedToken; |
91 | 6.27k | } |
92 | | |
93 | 1.32k | ExtensionsParser(const char *data, size_t length) { |
94 | 1.32k | const char *stop = data + length; |
95 | 1.32k | int token = 1; |
96 | | |
97 | | /* Ignore anything before permessage-deflate or x-webkit-deflate-frame */ |
98 | 3.42k | for (; token && token != TOK_PERMESSAGE_DEFLATE && token != TOK_X_WEBKIT_DEFLATE_FRAME; token = getToken(data, stop)); |
99 | | |
100 | | /* What protocol are we going to use? */ |
101 | 1.32k | perMessageDeflate = (token == TOK_PERMESSAGE_DEFLATE); |
102 | 1.32k | xWebKitDeflateFrame = (token == TOK_X_WEBKIT_DEFLATE_FRAME); |
103 | | |
104 | 4.18k | while ((token = getToken(data, stop))) { |
105 | 2.85k | switch (token) { |
106 | 2 | case TOK_X_WEBKIT_DEFLATE_FRAME: |
107 | | /* Duplicates not allowed/supported */ |
108 | 2 | return; |
109 | 192 | case TOK_NO_CONTEXT_TAKEOVER: |
110 | 192 | noContextTakeover = true; |
111 | 192 | break; |
112 | 260 | case TOK_MAX_WINDOW_BITS: |
113 | 260 | maxWindowBits = 1; |
114 | 260 | lastInteger = &maxWindowBits; |
115 | 260 | break; |
116 | 2 | case TOK_PERMESSAGE_DEFLATE: |
117 | | /* Duplicates not allowed/supported */ |
118 | 2 | return; |
119 | 192 | case TOK_SERVER_NO_CONTEXT_TAKEOVER: |
120 | 192 | serverNoContextTakeover = true; |
121 | 192 | break; |
122 | 192 | case TOK_CLIENT_NO_CONTEXT_TAKEOVER: |
123 | 192 | clientNoContextTakeover = true; |
124 | 192 | break; |
125 | 252 | case TOK_SERVER_MAX_WINDOW_BITS: |
126 | 252 | serverMaxWindowBits = 1; |
127 | 252 | lastInteger = &serverMaxWindowBits; |
128 | 252 | break; |
129 | 258 | case TOK_CLIENT_MAX_WINDOW_BITS: |
130 | 258 | clientMaxWindowBits = 1; |
131 | 258 | lastInteger = &clientMaxWindowBits; |
132 | 258 | break; |
133 | 1.50k | default: |
134 | 1.50k | if (token < 0 && lastInteger) { |
135 | 372 | *lastInteger = -token; |
136 | 372 | } |
137 | 1.50k | break; |
138 | 2.85k | } |
139 | 2.85k | } |
140 | 1.32k | } |
141 | | }; |
142 | | |
143 | | /* Takes what we (the server) wants, returns what we got */ |
144 | 1.99k | static inline std::tuple<bool, int, int, std::string_view> negotiateCompression(bool wantCompression, int wantedCompressionWindow, int wantedInflationWindow, std::string_view offer) { |
145 | | |
146 | | /* If we don't want compression then we are done here */ |
147 | 1.99k | if (!wantCompression) { |
148 | 664 | return {false, 0, 0, ""}; |
149 | 664 | } |
150 | | |
151 | 1.32k | ExtensionsParser ep(offer.data(), offer.length()); |
152 | | |
153 | 1.32k | static thread_local std::string response; |
154 | 1.32k | response = ""; |
155 | | |
156 | 1.32k | int compressionWindow = wantedCompressionWindow; |
157 | 1.32k | int inflationWindow = wantedInflationWindow; |
158 | 1.32k | bool compression = false; |
159 | | |
160 | 1.32k | if (ep.xWebKitDeflateFrame) { |
161 | | /* We now have compression */ |
162 | 68 | compression = true; |
163 | 68 | response = "x-webkit-deflate-frame"; |
164 | | |
165 | | /* If the other peer has DEMANDED us no sliding window, |
166 | | * we cannot compress with anything other than shared compressor */ |
167 | 68 | if (ep.noContextTakeover) { |
168 | | /* We must fail here right now (fix pub/sub) */ |
169 | 2 | #ifndef UWS_ALLOW_SHARED_AND_DEDICATED_COMPRESSOR_MIX |
170 | 2 | if (wantedCompressionWindow != 0) { |
171 | 1 | return {false, 0, 0, ""}; |
172 | 1 | } |
173 | 1 | #endif |
174 | | |
175 | 1 | compressionWindow = 0; |
176 | 1 | } |
177 | | |
178 | | /* If the other peer has DEMANDED us to use a limited sliding window, |
179 | | * we have to limit out compression sliding window */ |
180 | 67 | if (ep.maxWindowBits && ep.maxWindowBits < compressionWindow) { |
181 | 8 | compressionWindow = ep.maxWindowBits; |
182 | 8 | #ifndef UWS_ALLOW_8_WINDOW_BITS |
183 | | /* We cannot really deny this, so we have to disable compression in this case */ |
184 | 8 | if (compressionWindow == 8) { |
185 | 1 | return {false, 0, 0, ""}; |
186 | 1 | } |
187 | 8 | #endif |
188 | 8 | } |
189 | | |
190 | | /* We decide our own inflation sliding window (and their compression sliding window) */ |
191 | 66 | if (wantedInflationWindow < 15) { |
192 | 66 | if (!wantedInflationWindow) { |
193 | 66 | response += "; no_context_takeover"; |
194 | 66 | } else { |
195 | 0 | response += "; max_window_bits=" + std::to_string(wantedInflationWindow); |
196 | 0 | } |
197 | 66 | } |
198 | 1.26k | } else if (ep.perMessageDeflate) { |
199 | | /* We now have compression */ |
200 | 130 | compression = true; |
201 | 130 | response = "permessage-deflate"; |
202 | | |
203 | 130 | if (ep.clientNoContextTakeover) { |
204 | 2 | inflationWindow = 0; |
205 | 128 | } else if (ep.clientMaxWindowBits && ep.clientMaxWindowBits != 1) { |
206 | 62 | inflationWindow = std::min<int>(ep.clientMaxWindowBits, inflationWindow); |
207 | 62 | } |
208 | | |
209 | | /* Whatever we have now, write */ |
210 | 130 | if (inflationWindow < 15) { |
211 | 130 | if (!inflationWindow || !ep.clientMaxWindowBits) { |
212 | 130 | response += "; client_no_context_takeover"; |
213 | 130 | inflationWindow = 0; |
214 | 130 | } else { |
215 | 0 | response += "; client_max_window_bits=" + std::to_string(inflationWindow); |
216 | 0 | } |
217 | 130 | } |
218 | | |
219 | | /* This block basically lets the client lower it */ |
220 | 130 | if (ep.serverNoContextTakeover) { |
221 | | /* This is an important (temporary) fix since we haven't allowed |
222 | | * these two modes to mix, and pub/sub will not handle this case (yet) */ |
223 | | #ifdef UWS_ALLOW_SHARED_AND_DEDICATED_COMPRESSOR_MIX |
224 | | compressionWindow = 0; |
225 | | #endif |
226 | 128 | } else if (ep.serverMaxWindowBits) { |
227 | 60 | compressionWindow = std::min<int>(ep.serverMaxWindowBits, compressionWindow); |
228 | 60 | #ifndef UWS_ALLOW_8_WINDOW_BITS |
229 | | /* Zlib cannot do windowBits=8, memLevel=1 so we raise it up to 9 minimum */ |
230 | 60 | if (compressionWindow == 8) { |
231 | 1 | compressionWindow = 9; |
232 | 1 | } |
233 | 60 | #endif |
234 | 60 | } |
235 | | |
236 | | /* Whatever we have now, write */ |
237 | 130 | if (compressionWindow < 15) { |
238 | 130 | if (!compressionWindow) { |
239 | 65 | response += "; server_no_context_takeover"; |
240 | 65 | } else { |
241 | 65 | response += "; server_max_window_bits=" + std::to_string(compressionWindow); |
242 | 65 | } |
243 | 130 | } |
244 | 130 | } |
245 | | |
246 | | /* A final sanity check (this check does not actually catch too high values!) */ |
247 | 1.32k | if ((compressionWindow && compressionWindow < 8) || compressionWindow > 15 || (inflationWindow && inflationWindow < 8) || inflationWindow > 15) { |
248 | 8 | return {false, 0, 0, ""}; |
249 | 8 | } |
250 | | |
251 | 1.31k | return {compression, compressionWindow, inflationWindow, response}; |
252 | 1.32k | } |
253 | | |
254 | | } |
255 | | |
256 | | #endif // UWS_WEBSOCKETEXTENSIONS_H |