File
Blob: src/workerd/jsg/url.h
| 1 | #pragma once |
| 2 | #include <workerd/jsg/memory.h> |
| 3 | |
| 4 | #include <kj/common.h> |
| 5 | #include <kj/one-of.h> |
| 6 | #include <kj/string.h> |
| 7 | |
| 8 | namespace workerd::jsg { |
| 9 | |
| 10 | // A WHATWG-compliant URL implementation provided by ada-url. |
| 11 | class Url final { |
| 12 | public: |
| 13 | // Keep in sync with ada::scheme:type |
| 14 | enum class SchemeType { |
| 15 | HTTP = 0, |
| 16 | NOT_SPECIAL = 1, |
| 17 | HTTPS = 2, |
| 18 | WS = 3, |
| 19 | FTP = 4, |
| 20 | WSS = 5, |
| 21 | FILE = 6 |
| 22 | }; |
| 23 | |
| 24 | // Keep in sync with ada::url_host_type |
| 25 | enum class HostType { |
| 26 | DEFAULT = 0, |
| 27 | IPV4 = 1, |
| 28 | IPV6 = 2, |
| 29 | }; |
| 30 | |
| 31 | Url(decltype(nullptr)) {} |
| 32 | Url(Url&& other) = default; |
| 33 | KJ_DISALLOW_COPY(Url); |
| 34 | |
| 35 | Url& operator=(Url&& other) = default; |
| 36 | bool operator==(const Url& other) const KJ_WARN_UNUSED_RESULT; |
| 37 | |
| 38 | enum class EquivalenceOption { |
| 39 | DEFAULT = 0, |
| 40 | // When set, the fragment/hash portion of the URL will be ignored when comparing or |
| 41 | // cloning URLs. |
| 42 | IGNORE_FRAGMENTS = 1 << 0, |
| 43 | // When set, the search portion of the URL will be ignored when comparing or cloning URLs. |
| 44 | IGNORE_SEARCH = 1 << 1, |
| 45 | // When set, the pathname portion of the URL will be normalized by percent-decoding |
| 46 | // then re-encoding the pathname. This is useful when comparing URLs that may have |
| 47 | // different, but equivalent percent-encoded paths. e.g. %66oo and foo are equivalent. |
| 48 | NORMALIZE_PATH = 1 << 2, |
| 49 | }; |
| 50 | |
| 51 | bool equal(const Url& other, |
| 52 | EquivalenceOption option = EquivalenceOption::DEFAULT) const KJ_WARN_UNUSED_RESULT; |
| 53 | |
| 54 | // Returns true if the given input can be successfully parsed as a URL. This is generally |
| 55 | // more performant than using tryParse and checking for a kj::none result if all you want |
| 56 | // to do is verify that the input is parseable. If you actually want to parse and use the |
| 57 | // result, use tryParse instead. |
| 58 | static bool canParse(kj::ArrayPtr<const char> input, |
| 59 | kj::Maybe<kj::ArrayPtr<const char>> base = kj::none) KJ_WARN_UNUSED_RESULT; |
| 60 | static bool canParse( |
| 61 | kj::StringPtr input, kj::Maybe<kj::StringPtr> base = kj::none) KJ_WARN_UNUSED_RESULT; |
| 62 | |
| 63 | static kj::Maybe<Url> tryParse(kj::ArrayPtr<const char> input, |
| 64 | kj::Maybe<kj::ArrayPtr<const char>> base = kj::none) KJ_WARN_UNUSED_RESULT; |
| 65 | static kj::Maybe<Url> tryParse( |
| 66 | kj::StringPtr input, kj::Maybe<kj::StringPtr> base = kj::none) KJ_WARN_UNUSED_RESULT; |
| 67 | |
| 68 | kj::Array<const char> getOrigin() const KJ_WARN_UNUSED_RESULT; |
| 69 | kj::ArrayPtr<const char> getProtocol() const KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT; |
| 70 | kj::ArrayPtr<const char> getHref() const KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT; |
| 71 | kj::ArrayPtr<const char> getPathname() const KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT; |
| 72 | kj::ArrayPtr<const char> getUsername() const KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT; |
| 73 | kj::ArrayPtr<const char> getPassword() const KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT; |
| 74 | kj::ArrayPtr<const char> getPort() const KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT; |
| 75 | kj::ArrayPtr<const char> getHash() const KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT; |
| 76 | kj::ArrayPtr<const char> getHost() const KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT; |
| 77 | kj::ArrayPtr<const char> getHostname() const KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT; |
| 78 | kj::ArrayPtr<const char> getSearch() const KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT; |
| 79 | |
| 80 | bool setHref(kj::ArrayPtr<const char> value); |
| 81 | bool setHost(kj::ArrayPtr<const char> value); |
| 82 | bool setHostname(kj::ArrayPtr<const char> value); |
| 83 | bool setProtocol(kj::ArrayPtr<const char> value); |
| 84 | bool setUsername(kj::ArrayPtr<const char> value); |
| 85 | bool setPassword(kj::ArrayPtr<const char> value); |
| 86 | bool setPort(kj::Maybe<kj::ArrayPtr<const char>> value); |
| 87 | bool setPathname(kj::ArrayPtr<const char> value); |
| 88 | void setSearch(kj::Maybe<kj::ArrayPtr<const char>> value); |
| 89 | void setHash(kj::Maybe<kj::ArrayPtr<const char>> value); |
| 90 | |
| 91 | kj::uint hashCode() const; |
| 92 | |
| 93 | kj::Maybe<Url> resolve(kj::ArrayPtr<const char> input) KJ_WARN_UNUSED_RESULT; |
| 94 | |
| 95 | // Copies this Url. If the option is set of EquivalenceOption::IGNORE_FRAGMENTS, the |
| 96 | // copied Url will clear any fragment/hash that exists. |
| 97 | Url clone(EquivalenceOption option = EquivalenceOption::DEFAULT) const KJ_WARN_UNUSED_RESULT; |
| 98 | |
| 99 | // Resolve the input relative to this URL |
| 100 | kj::Maybe<Url> tryResolve(kj::ArrayPtr<const char> input) const KJ_WARN_UNUSED_RESULT; |
| 101 | |
| 102 | enum class RelativeOption { |
| 103 | DEFAULT, |
| 104 | // If the URL ends with a trailing slash, remove it before determining the basename. |
| 105 | STRIP_TAILING_SLASHES, |
| 106 | }; |
| 107 | struct Relative; |
| 108 | // Given this URL, returns a struct that is a basename and a base Url pair |
| 109 | // such that base.tryResolve(basename) is equivalent to this URL. Query |
| 110 | // parameters and fragments are not preserved. |
| 111 | Relative getRelative(RelativeOption option = RelativeOption::DEFAULT) const; |
| 112 | |
| 113 | // Returns the parent URL of this URL, which is the URL with the last path component |
| 114 | // removed and the trailing slash removed if it exists. For instance, if the URL is |
| 115 | // "https://example.com/foo/bar/baz", the parent URL will be "https://example.com/foo/bar". |
| 116 | // If the URL has no parent (e.g. "https://example.com/") kj::none is returned. |
| 117 | kj::Maybe<jsg::Url> getParent() const KJ_WARN_UNUSED_RESULT; |
| 118 | |
| 119 | HostType getHostType() const; |
| 120 | SchemeType getSchemeType() const; |
| 121 | |
| 122 | // Convert an ASCII hostname to Unicode. |
| 123 | static kj::Array<const char> idnToUnicode(kj::ArrayPtr<const char> value) KJ_WARN_UNUSED_RESULT; |
| 124 | |
| 125 | // Convert a Unicode hostname to ASCII. |
| 126 | static kj::Array<const char> idnToAscii(kj::ArrayPtr<const char> value) KJ_WARN_UNUSED_RESULT; |
| 127 | |
| 128 | static bool isSpecialScheme(kj::StringPtr protocol); |
| 129 | static bool isSpecialSchemeDefaultPort(kj::StringPtr protocol, kj::StringPtr port); |
| 130 | |
| 131 | JSG_MEMORY_INFO(Url) { |
| 132 | tracker.trackFieldWithSize("inner", |
| 133 | getProtocol().size() + getUsername().size() + getPassword().size() + getHost().size() + |
| 134 | getPathname().size() + getHash().size() + getSearch().size()); |
| 135 | } |
| 136 | |
| 137 | static kj::Array<kj::byte> percentDecode(kj::ArrayPtr<const kj::byte> input); |
| 138 | |
| 139 | private: |
| 140 | Url(kj::Own<void> inner); |
| 141 | kj::Own<void> inner; |
| 142 | }; |
| 143 | |
| 144 | struct Url::Relative { |
| 145 | Url base; |
| 146 | kj::String name; |
| 147 | }; |
| 148 | |
| 149 | constexpr Url::EquivalenceOption operator|(Url::EquivalenceOption a, Url::EquivalenceOption b) { |
| 150 | return static_cast<Url::EquivalenceOption>(static_cast<int>(a) | static_cast<int>(b)); |
| 151 | } |
| 152 | constexpr Url::EquivalenceOption operator&(Url::EquivalenceOption a, Url::EquivalenceOption b) { |
| 153 | return static_cast<Url::EquivalenceOption>(static_cast<int>(a) & static_cast<int>(b)); |
| 154 | } |
| 155 | |
| 156 | class UrlSearchParams final { |
| 157 | public: |
| 158 | class KeyIterator final { |
| 159 | public: |
| 160 | bool hasNext() const; |
| 161 | kj::Maybe<kj::ArrayPtr<const char>> next() const; |
| 162 | |
| 163 | private: |
| 164 | KeyIterator(kj::Own<void> inner); |
| 165 | kj::Own<void> inner; |
| 166 | friend class UrlSearchParams; |
| 167 | }; |
| 168 | class ValueIterator final { |
| 169 | public: |
| 170 | bool hasNext() const; |
| 171 | kj::Maybe<kj::ArrayPtr<const char>> next() const; |
| 172 | |
| 173 | private: |
| 174 | ValueIterator(kj::Own<void> inner); |
| 175 | kj::Own<void> inner; |
| 176 | friend class UrlSearchParams; |
| 177 | }; |
| 178 | class EntryIterator final { |
| 179 | public: |
| 180 | struct Entry { |
| 181 | kj::ArrayPtr<const char> key; |
| 182 | kj::ArrayPtr<const char> value; |
| 183 | }; |
| 184 | bool hasNext() const; |
| 185 | kj::Maybe<Entry> next() const; |
| 186 | |
| 187 | private: |
| 188 | EntryIterator(kj::Own<void> inner); |
| 189 | kj::Own<void> inner; |
| 190 | friend class UrlSearchParams; |
| 191 | }; |
| 192 | |
| 193 | UrlSearchParams(); |
| 194 | UrlSearchParams(UrlSearchParams&& other) = default; |
| 195 | KJ_DISALLOW_COPY(UrlSearchParams); |
| 196 | |
| 197 | UrlSearchParams& operator=(UrlSearchParams&& other) = default; |
| 198 | bool operator==(const UrlSearchParams& other) const KJ_WARN_UNUSED_RESULT; |
| 199 | |
| 200 | static kj::Maybe<UrlSearchParams> tryParse(kj::ArrayPtr<const char> input) KJ_WARN_UNUSED_RESULT; |
| 201 | |
| 202 | size_t size() const; |
| 203 | void append(kj::ArrayPtr<const char> key, kj::ArrayPtr<const char> value); |
| 204 | void set(kj::ArrayPtr<const char> key, kj::ArrayPtr<const char> value); |
| 205 | void delete_( |
| 206 | kj::ArrayPtr<const char> key, kj::Maybe<kj::ArrayPtr<const char>> maybeValue = kj::none); |
| 207 | bool has(kj::ArrayPtr<const char> key, |
| 208 | kj::Maybe<kj::ArrayPtr<const char>> maybeValue = kj::none) const KJ_WARN_UNUSED_RESULT; |
| 209 | kj::Maybe<kj::ArrayPtr<const char>> get( |
| 210 | kj::ArrayPtr<const char> key) const KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT; |
| 211 | kj::Array<kj::ArrayPtr<const char>> getAll( |
| 212 | kj::ArrayPtr<const char> key) const KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT; |
| 213 | void sort(); |
| 214 | KeyIterator getKeys() const KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT; |
| 215 | ValueIterator getValues() const KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT; |
| 216 | EntryIterator getEntries() const KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT; |
| 217 | |
| 218 | kj::Array<const char> toStr() const KJ_WARN_UNUSED_RESULT; |
| 219 | |
| 220 | JSG_MEMORY_INFO(Url) { |
| 221 | tracker.trackField("inner", toStr()); |
| 222 | } |
| 223 | |
| 224 | void reset(kj::Maybe<kj::ArrayPtr<const char>> input); |
| 225 | |
| 226 | private: |
| 227 | UrlSearchParams(kj::Own<void> inner); |
| 228 | kj::Own<void> inner; |
| 229 | }; |
| 230 | |
| 231 | inline kj::String KJ_STRINGIFY(const Url& url) { |
| 232 | return kj::str(url.getHref()); |
| 233 | } |
| 234 | |
| 235 | inline kj::String KJ_STRINGIFY(const UrlSearchParams& searchParams) { |
| 236 | return kj::str(searchParams.toStr()); |
| 237 | } |
| 238 | |
| 239 | // ====================================================================================== |
| 240 | |
| 241 | // Encapsulates a parsed URLPattern. |
| 242 | // @see https://wicg.github.io/urlpattern |
| 243 | class UrlPattern final { |
| 244 | public: |
| 245 | // If the value is T, the operation is successful. |
| 246 | // If the value is kj::String, that's an Error message. |
| 247 | template <typename T> |
| 248 | using Result = kj::OneOf<T, kj::String>; |
| 249 | |
| 250 | // An individual, compiled component of a URLPattern. |
| 251 | class Component final { |
| 252 | public: |
| 253 | Component(kj::String pattern, kj::String regex, kj::Array<kj::String> names); |
| 254 | |
| 255 | Component(Component&&) = default; |
| 256 | Component& operator=(Component&&) = default; |
| 257 | KJ_DISALLOW_COPY(Component); |
| 258 | |
| 259 | inline kj::StringPtr getPattern() const KJ_LIFETIMEBOUND { |
| 260 | return pattern; |
| 261 | } |
| 262 | inline kj::StringPtr getRegex() const KJ_LIFETIMEBOUND { |
| 263 | return regex; |
| 264 | } |
| 265 | inline kj::ArrayPtr<const kj::String> getNames() const KJ_LIFETIMEBOUND { |
| 266 | return names.asPtr(); |
| 267 | } |
| 268 | |
| 269 | JSG_MEMORY_INFO(Component) { |
| 270 | tracker.trackField("pattern", pattern); |
| 271 | tracker.trackField("regex", regex); |
| 272 | for (const auto& name: names) { |
| 273 | tracker.trackField("name", name); |
| 274 | } |
| 275 | } |
| 276 | |
| 277 | private: |
| 278 | // The normalized pattern for this component. |
| 279 | kj::String pattern = nullptr; |
| 280 | |
| 281 | // The generated JavaScript regular expression for this component. |
| 282 | kj::String regex = nullptr; |
| 283 | |
| 284 | // The list of sub-component names extracted for this component. |
| 285 | kj::Array<kj::String> names = nullptr; |
| 286 | }; |
| 287 | |
| 288 | // A structure providing matching patterns for individual components of a URL. |
| 289 | struct Init { |
| 290 | kj::Maybe<kj::String> protocol; |
| 291 | kj::Maybe<kj::String> username; |
| 292 | kj::Maybe<kj::String> password; |
| 293 | kj::Maybe<kj::String> hostname; |
| 294 | kj::Maybe<kj::String> port; |
| 295 | kj::Maybe<kj::String> pathname; |
| 296 | kj::Maybe<kj::String> search; |
| 297 | kj::Maybe<kj::String> hash; |
| 298 | kj::Maybe<kj::String> baseUrl; |
| 299 | }; |
| 300 | |
| 301 | struct ProcessInitOptions { |
| 302 | enum class Mode { |
| 303 | PATTERN, |
| 304 | URL, |
| 305 | }; |
| 306 | Mode mode = Mode::PATTERN; |
| 307 | kj::Maybe<kj::StringPtr> protocol = kj::none; |
| 308 | kj::Maybe<kj::StringPtr> username = kj::none; |
| 309 | kj::Maybe<kj::StringPtr> password = kj::none; |
| 310 | kj::Maybe<kj::StringPtr> hostname = kj::none; |
| 311 | kj::Maybe<kj::StringPtr> port = kj::none; |
| 312 | kj::Maybe<kj::StringPtr> pathname = kj::none; |
| 313 | kj::Maybe<kj::StringPtr> search = kj::none; |
| 314 | kj::Maybe<kj::StringPtr> hash = kj::none; |
| 315 | }; |
| 316 | |
| 317 | // Processes the given init according to the specified mode and options. |
| 318 | // If a kj::String is returned, then processing failed and the string |
| 319 | // is the description to include in the error message (if any). |
| 320 | static Result<Init> processInit( |
| 321 | Init init, kj::Maybe<ProcessInitOptions> options = kj::none) KJ_WARN_UNUSED_RESULT; |
| 322 | |
| 323 | struct CompileOptions { |
| 324 | // The base URL to use. Only used in the compile(kj::StringPtr, ...) variant. |
| 325 | kj::Maybe<kj::StringPtr> baseUrl; |
| 326 | bool ignoreCase = false; |
| 327 | }; |
| 328 | |
| 329 | static Result<UrlPattern> tryCompile( |
| 330 | kj::StringPtr, kj::Maybe<CompileOptions> = kj::none) KJ_WARN_UNUSED_RESULT; |
| 331 | static Result<UrlPattern> tryCompile( |
| 332 | Init init, kj::Maybe<CompileOptions> = kj::none) KJ_WARN_UNUSED_RESULT; |
| 333 | |
| 334 | UrlPattern(UrlPattern&&) = default; |
| 335 | UrlPattern& operator=(UrlPattern&&) = default; |
| 336 | KJ_DISALLOW_COPY(UrlPattern); |
| 337 | |
| 338 | inline const Component& getProtocol() const KJ_LIFETIMEBOUND { |
| 339 | return protocol; |
| 340 | } |
| 341 | inline const Component& getUsername() const KJ_LIFETIMEBOUND { |
| 342 | return username; |
| 343 | } |
| 344 | inline const Component& getPassword() const KJ_LIFETIMEBOUND { |
| 345 | return password; |
| 346 | } |
| 347 | inline const Component& getHostname() const KJ_LIFETIMEBOUND { |
| 348 | return hostname; |
| 349 | } |
| 350 | inline const Component& getPort() const KJ_LIFETIMEBOUND { |
| 351 | return port; |
| 352 | } |
| 353 | inline const Component& getPathname() const KJ_LIFETIMEBOUND { |
| 354 | return pathname; |
| 355 | } |
| 356 | inline const Component& getSearch() const KJ_LIFETIMEBOUND { |
| 357 | return search; |
| 358 | } |
| 359 | inline const Component& getHash() const KJ_LIFETIMEBOUND { |
| 360 | return hash; |
| 361 | } |
| 362 | |
| 363 | // If ignoreCase is true, the JavaScript regular expression created for each pattern |
| 364 | // must use the `vi` flag. Otherwise, they must use the `v` flag. |
| 365 | inline bool getIgnoreCase() const { |
| 366 | return ignoreCase; |
| 367 | } |
| 368 | |
| 369 | JSG_MEMORY_INFO(UrlPattern) { |
| 370 | tracker.trackField("protocol", protocol); |
| 371 | tracker.trackField("username", username); |
| 372 | tracker.trackField("password", password); |
| 373 | tracker.trackField("hostname", hostname); |
| 374 | tracker.trackField("port", port); |
| 375 | tracker.trackField("pathname", pathname); |
| 376 | tracker.trackField("search", search); |
| 377 | tracker.trackField("hash", hash); |
| 378 | } |
| 379 | |
| 380 | private: |
| 381 | UrlPattern(kj::Array<Component> components, bool ignoreCase); |
| 382 | |
| 383 | Component protocol; |
| 384 | Component username; |
| 385 | Component password; |
| 386 | Component hostname; |
| 387 | Component port; |
| 388 | Component pathname; |
| 389 | Component search; |
| 390 | Component hash; |
| 391 | bool ignoreCase; |
| 392 | |
| 393 | static Result<UrlPattern> tryCompileInit(UrlPattern::Init init, const CompileOptions& options); |
| 394 | }; |
| 395 | } // namespace workerd::jsg |
| 396 | |
| 397 | // Append _url to a string literal to create a parsed URL. An assert will be triggered |
| 398 | // if the value cannot be parsed successfully. |
| 399 | const workerd::jsg::Url operator""_url(const char* str, size_t size) KJ_WARN_UNUSED_RESULT; |