File
Blob: src/workerd/api/util.c++
| 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 | |
| 14 | namespace workerd::api { |
| 15 | namespace { |
| 16 | |
| 17 | kj::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 | |
| 34 | void 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 | |
| 49 | kj::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 | |
| 56 | kj::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 | |
| 68 | namespace { |
| 69 | |
| 70 | template <typename Func> |
| 71 | auto 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 | |
| 94 | kj::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 | |
| 127 | kj::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 | |
| 195 | kj::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 | |
| 203 | void 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 | |
| 218 | kj::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 | |
| 230 | kj::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. |
| 242 | kj::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 |