Skip to content
File

Blob: src/workerd/api/util.c++

8.3 KB
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5#include "util.h"
6 
7#include "simdutf.h"
8 
9#include <workerd/util/mimetype.h>
10#include <workerd/util/strings.h>
11 
12#include <kj/encoding.h>
13 
14namespace workerd::api {
15namespace {
16 
17kj::ArrayPtr<const char> split(kj::ArrayPtr<const char>& text, char c) {
18 // TODO(cleanup): Modified version of split() found in kj/compat/url.c++.
19 
20 for (auto i: kj::indices(text)) {
21 if (text[i] == c) {
22 kj::ArrayPtr<const char> result = text.first(i);
23 text = text.slice(i + 1, text.size());
24 return result;
25 }
26 }
27 auto result = text;
28 text = {};
29 return result;
30}
31 
32} // namespace
33 
34void parseQueryString(kj::Vector<kj::Url::QueryParam>& query,
35 kj::ArrayPtr<const char> text,
36 bool skipLeadingQuestionMark) {
37 if (skipLeadingQuestionMark && text.size() > 0 && text[0] == '?') {
38 text = text.slice(1, text.size());
39 }
40 
41 while (text.size() > 0) {
42 auto value = split(text, '&');
43 if (value.size() == 0) continue;
44 auto name = split(value, '=');
45 query.add(kj::Url::QueryParam{kj::decodeWwwForm(name), kj::decodeWwwForm(value)});
46 }
47}
48 
49kj::Maybe<kj::String> readContentTypeParameter(kj::StringPtr contentType, kj::StringPtr param) {
50 KJ_IF_SOME(parsed, MimeType::tryParse(contentType)) {
51 return parsed.params().find(toLower(param)).map([](auto& value) { return kj::str(value); });
52 }
53 return kj::none;
54}
55 
56kj::Maybe<kj::Exception> translateKjException(
57 const kj::Exception& exception, std::initializer_list<ErrorTranslation> translations) {
58 for (auto& t: translations) {
59 if (exception.getDescription().contains(t.kjDescription)) {
60 return kj::Exception(kj::Exception::Type::FAILED, __FILE__, __LINE__,
61 kj::str(JSG_EXCEPTION(TypeError) ": ", t.jsDescription));
62 }
63 }
64 
65 return kj::none;
66}
67 
68namespace {
69 
70template <typename Func>
71auto translateTeeErrors(Func&& f) -> decltype(kj::fwd<Func>(f)()) {
72 try {
73 co_return co_await f();
74 } catch (...) {
75 auto exception = kj::getCaughtExceptionAsKj();
76 KJ_IF_SOME(e,
77 translateKjException(exception,
78 {
79 {"tee buffer size limit exceeded"_kj,
80 "ReadableStream.tee() buffer limit exceeded. This error usually occurs when a Request or "
81 "Response with a large body is cloned, then only one of the clones is read, forcing "
82 "the Workers runtime to buffer the entire body in memory. To fix this issue, remove "
83 "unnecessary calls to Request/Response.clone() and ReadableStream.tee(), and always read "
84 "clones/tees in parallel."_kj},
85 })) {
86 kj::throwFatalException(kj::mv(e));
87 }
88 kj::throwFatalException(kj::mv(exception));
89 }
90}
91 
92} // namespace
93 
94kj::Own<kj::AsyncInputStream> newTeeErrorAdapter(kj::Own<kj::AsyncInputStream> inner) {
95 class Adapter final: public kj::AsyncInputStream {
96 public:
97 explicit Adapter(kj::Own<AsyncInputStream> inner): inner(kj::mv(inner)) {}
98 
99 kj::Promise<size_t> tryRead(void* buffer, size_t minBytes, size_t maxBytes) override {
100 return translateTeeErrors([&] { return inner->tryRead(buffer, minBytes, maxBytes); });
101 }
102 
103 kj::Maybe<uint64_t> tryGetLength() override {
104 return inner->tryGetLength();
105 };
106 
107 kj::Promise<uint64_t> pumpTo(kj::AsyncOutputStream& output, uint64_t amount) override {
108 return translateTeeErrors([&] { return inner->pumpTo(output, amount); });
109 }
110 
111 kj::Maybe<kj::Own<kj::AsyncInputStream>> tryTee(uint64_t limit) override {
112 return inner->tryTee(limit);
113 }
114 
115 private:
116 kj::Own<AsyncInputStream> inner;
117 };
118 
119 if (dynamic_cast<Adapter*>(inner.get()) != nullptr) {
120 // HACK: Don't double-wrap. This can otherwise happen if we tee a tee.
121 return kj::mv(inner);
122 } else {
123 return kj::heap<Adapter>(kj::mv(inner));
124 }
125}
126 
127kj::String redactUrl(kj::StringPtr url) {
128 kj::Vector<char> redacted(url.size() + 1);
129 const char* spanStart = url.begin();
130 bool sawNonHexChar = false;
131 uint digitCount = 0;
132 uint upperCount = 0;
133 uint lowerCount = 0;
134 uint hexDigitCount = 0;
135 
136 auto maybeRedactSpan = [&](kj::ArrayPtr<const char> span) {
137 bool isHexId = (hexDigitCount >= 32 && !sawNonHexChar);
138 bool probablyBase64Id =
139 (span.size() >= 21 && digitCount >= 2 && upperCount >= 2 && lowerCount >= 2);
140 
141 if (isHexId || probablyBase64Id) {
142 redacted.addAll("REDACTED"_kj);
143 } else {
144 redacted.addAll(span);
145 }
146 };
147 
148 for (const char& c: url) {
149 uint8_t lookup = kCharLookupTable[static_cast<const kj::byte>(c)];
150 bool isSep = lookup & CharAttributeFlag::SEPARATOR;
151 bool isAlphaUpper = lookup & CharAttributeFlag::UPPER_CASE;
152 bool isAlphaLower = lookup & CharAttributeFlag::LOWER_CASE;
153 bool isDigit = lookup & CharAttributeFlag::DIGIT;
154 bool isHex = lookup & CharAttributeFlag::HEX;
155 
156 // These extra characters are used in the regular and url-safe versions of
157 // base64, but might also be used for GUID-style separators in hex ids.
158 // Regular base64 also includes '/', which we don't try to match here due
159 // to its prevalence in URLs. Likewise, we ignore the base64 "=" padding
160 // character.
161 
162 if (isAlphaUpper || isAlphaLower || isDigit || isSep) {
163 if (isHex) {
164 hexDigitCount++;
165 }
166 if (!isHex && !isSep) {
167 sawNonHexChar = true;
168 }
169 if (isAlphaUpper) {
170 upperCount++;
171 }
172 if (isAlphaLower) {
173 lowerCount++;
174 }
175 if (isDigit) {
176 digitCount++;
177 }
178 } else {
179 maybeRedactSpan(kj::ArrayPtr<const char>(spanStart, &c));
180 redacted.add(c);
181 spanStart = &c + 1;
182 hexDigitCount = 0;
183 digitCount = 0;
184 upperCount = 0;
185 lowerCount = 0;
186 sawNonHexChar = false;
187 }
188 }
189 maybeRedactSpan(kj::ArrayPtr<const char>(spanStart, url.end()));
190 redacted.add('\0');
191 
192 return kj::String(redacted.releaseAsArray());
193}
194 
195kj::Maybe<jsg::V8Ref<v8::Object>> cloneRequestCf(
196 jsg::Lock& js, kj::Maybe<jsg::V8Ref<v8::Object>> maybeCf) {
197 KJ_IF_SOME(cf, maybeCf) {
198 return cf.deepClone(js);
199 }
200 return kj::none;
201}
202 
203void maybeWarnIfNotText(jsg::Lock& js, kj::StringPtr str) {
204 KJ_IF_SOME(parsed, MimeType::tryParse(str)) {
205 if (MimeType::isText(parsed)) return;
206 }
207 // A common mistake is to call .text() on non-text content, e.g. because you're implementing a
208 // search-and-replace across your whole site and you forgot that it'll apply to images too.
209 // When running with an inspector, let's warn the developer if they do this.
210 js.logWarning(
211 kj::str("Called .text() on an HTTP body which does not appear to be text. The body's "
212 "Content-Type is \"",
213 str,
214 "\". The result will probably be corrupted. Consider "
215 "checking the Content-Type header before interpreting entities as text."));
216}
217 
218kj::String fastEncodeBase64Url(kj::ArrayPtr<const byte> bytes) {
219 if (KJ_UNLIKELY(bytes.size() == 0)) {
220 return {};
221 }
222 auto expected_length = simdutf::base64_length_from_binary(bytes.size(), simdutf::base64_url);
223 auto output = kj::heapArray<char>(expected_length + 1);
224 auto actual_length = simdutf::binary_to_base64(
225 bytes.asChars().begin(), bytes.size(), output.asChars().begin(), simdutf::base64_url);
226 output[actual_length] = '\0';
227 return kj::String(kj::mv(output));
228}
229 
230kj::Array<char16_t> fastEncodeUtf16(kj::ArrayPtr<const char> bytes) {
231 if (KJ_UNLIKELY(bytes.size() == 0)) {
232 return {};
233 }
234 auto expected_length = simdutf::utf16_length_from_utf8(bytes.asChars().begin(), bytes.size());
235 auto output = kj::heapArray<char16_t>(expected_length);
236 auto actual_length =
237 simdutf::convert_utf8_to_utf16(bytes.asChars().begin(), bytes.size(), output.begin());
238 return output.first(actual_length).attach(kj::mv(output));
239}
240 
241// URI-encode control characters and spaces.
242kj::String uriEncodeControlChars(kj::ArrayPtr<const byte> bytes) {
243 // TODO(cleanup): Once this is deployed, update open-source KJ HTTP to do this automatically.
244 const char HEX_DIGITS_URI[] = "0123456789ABCDEF";
245 
246 kj::Vector<char> result(bytes.size() + 1);
247 for (byte b: bytes) {
248 if (b > 0x20) {
249 result.add(b);
250 } else {
251 result.add('%');
252 result.add(HEX_DIGITS_URI[b / 16]);
253 result.add(HEX_DIGITS_URI[b % 16]);
254 }
255 }
256 result.add('\0');
257 return kj::String(result.releaseAsArray());
258}
259 
260} // namespace workerd::api