mirror of
https://github.com/square/okhttp.git
synced 2025-08-08 23:42:08 +03:00
HttpUrl multi platform (#7549)
Not perfect implementation, two main issues. - Charset and IDN not supported in nonJvm implementation - Should have more common code, with just the TODO parts in expect/actual blocks.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -693,7 +693,6 @@ public final class okhttp3/HttpUrl {
|
||||
}
|
||||
|
||||
public final class okhttp3/HttpUrl$Builder {
|
||||
public static final field Companion Lokhttp3/HttpUrl$Builder$Companion;
|
||||
public fun <init> ()V
|
||||
public final fun addEncodedPathSegment (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder;
|
||||
public final fun addEncodedPathSegments (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder;
|
||||
@@ -724,9 +723,6 @@ public final class okhttp3/HttpUrl$Builder {
|
||||
public final fun username (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder;
|
||||
}
|
||||
|
||||
public final class okhttp3/HttpUrl$Builder$Companion {
|
||||
}
|
||||
|
||||
public final class okhttp3/HttpUrl$Companion {
|
||||
public final fun -deprecated_get (Ljava/lang/String;)Lokhttp3/HttpUrl;
|
||||
public final fun -deprecated_get (Ljava/net/URI;)Lokhttp3/HttpUrl;
|
||||
|
679
okhttp/src/commonMain/kotlin/okhttp3/HttpUrl.kt
Normal file
679
okhttp/src/commonMain/kotlin/okhttp3/HttpUrl.kt
Normal file
@@ -0,0 +1,679 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package okhttp3
|
||||
|
||||
import kotlin.jvm.JvmName
|
||||
import kotlin.jvm.JvmStatic
|
||||
|
||||
/**
|
||||
* A uniform resource locator (URL) with a scheme of either `http` or `https`. Use this class to
|
||||
* compose and decompose Internet addresses. For example, this code will compose and print a URL for
|
||||
* Google search:
|
||||
*
|
||||
* ```java
|
||||
* HttpUrl url = new HttpUrl.Builder()
|
||||
* .scheme("https")
|
||||
* .host("www.google.com")
|
||||
* .addPathSegment("search")
|
||||
* .addQueryParameter("q", "polar bears")
|
||||
* .build();
|
||||
* System.out.println(url);
|
||||
* ```
|
||||
*
|
||||
* which prints:
|
||||
*
|
||||
* ```
|
||||
* https://www.google.com/search?q=polar%20bears
|
||||
* ```
|
||||
*
|
||||
* As another example, this code prints the human-readable query parameters of a Twitter search:
|
||||
*
|
||||
* ```java
|
||||
* HttpUrl url = HttpUrl.parse("https://twitter.com/search?q=cute%20%23puppies&f=images");
|
||||
* for (int i = 0, size = url.querySize(); i < size; i++) {
|
||||
* System.out.println(url.queryParameterName(i) + ": " + url.queryParameterValue(i));
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* which prints:
|
||||
*
|
||||
* ```
|
||||
* q: cute #puppies
|
||||
* f: images
|
||||
* ```
|
||||
*
|
||||
* In addition to composing URLs from their component parts and decomposing URLs into their
|
||||
* component parts, this class implements relative URL resolution: what address you'd reach by
|
||||
* clicking a relative link on a specified page. For example:
|
||||
*
|
||||
* ```java
|
||||
* HttpUrl base = HttpUrl.parse("https://www.youtube.com/user/WatchTheDaily/videos");
|
||||
* HttpUrl link = base.resolve("../../watch?v=cbP2N1BQdYc");
|
||||
* System.out.println(link);
|
||||
* ```
|
||||
*
|
||||
* which prints:
|
||||
*
|
||||
* ```
|
||||
* https://www.youtube.com/watch?v=cbP2N1BQdYc
|
||||
* ```
|
||||
*
|
||||
* ## What's in a URL?
|
||||
*
|
||||
* A URL has several components.
|
||||
*
|
||||
* ### Scheme
|
||||
*
|
||||
* Sometimes referred to as *protocol*, A URL's scheme describes what mechanism should be used to
|
||||
* retrieve the resource. Although URLs have many schemes (`mailto`, `file`, `ftp`), this class only
|
||||
* supports `http` and `https`. Use [java.net.URI][URI] for URLs with arbitrary schemes.
|
||||
*
|
||||
* ### Username and Password
|
||||
*
|
||||
* Username and password are either present, or the empty string `""` if absent. This class offers
|
||||
* no mechanism to differentiate empty from absent. Neither of these components are popular in
|
||||
* practice. Typically HTTP applications use other mechanisms for user identification and
|
||||
* authentication.
|
||||
*
|
||||
* ### Host
|
||||
*
|
||||
* The host identifies the webserver that serves the URL's resource. It is either a hostname like
|
||||
* `square.com` or `localhost`, an IPv4 address like `192.168.0.1`, or an IPv6 address like `::1`.
|
||||
*
|
||||
* Usually a webserver is reachable with multiple identifiers: its IP addresses, registered
|
||||
* domain names, and even `localhost` when connecting from the server itself. Each of a web server's
|
||||
* names is a distinct URL and they are not interchangeable. For example, even if
|
||||
* `http://square.github.io/dagger` and `http://google.github.io/dagger` are served by the same IP
|
||||
* address, the two URLs identify different resources.
|
||||
*
|
||||
* ### Port
|
||||
*
|
||||
* The port used to connect to the web server. By default this is 80 for HTTP and 443 for HTTPS.
|
||||
* This class never returns -1 for the port: if no port is explicitly specified in the URL then the
|
||||
* scheme's default is used.
|
||||
*
|
||||
* ### Path
|
||||
*
|
||||
* The path identifies a specific resource on the host. Paths have a hierarchical structure like
|
||||
* "/square/okhttp/issues/1486" and decompose into a list of segments like `["square", "okhttp",
|
||||
* "issues", "1486"]`.
|
||||
*
|
||||
* This class offers methods to compose and decompose paths by segment. It composes each path
|
||||
* from a list of segments by alternating between "/" and the encoded segment. For example the
|
||||
* segments `["a", "b"]` build "/a/b" and the segments `["a", "b", ""]` build "/a/b/".
|
||||
*
|
||||
* If a path's last segment is the empty string then the path ends with "/". This class always
|
||||
* builds non-empty paths: if the path is omitted it defaults to "/". The default path's segment
|
||||
* list is a single empty string: `[""]`.
|
||||
*
|
||||
* ### Query
|
||||
*
|
||||
* The query is optional: it can be null, empty, or non-empty. For many HTTP URLs the query string
|
||||
* is subdivided into a collection of name-value parameters. This class offers methods to set the
|
||||
* query as the single string, or as individual name-value parameters. With name-value parameters
|
||||
* the values are optional and names may be repeated.
|
||||
*
|
||||
* ### Fragment
|
||||
*
|
||||
* The fragment is optional: it can be null, empty, or non-empty. Unlike host, port, path, and
|
||||
* query the fragment is not sent to the webserver: it's private to the client.
|
||||
*
|
||||
* ## Encoding
|
||||
*
|
||||
* Each component must be encoded before it is embedded in the complete URL. As we saw above, the
|
||||
* string `cute #puppies` is encoded as `cute%20%23puppies` when used as a query parameter value.
|
||||
*
|
||||
* ### Percent encoding
|
||||
*
|
||||
* Percent encoding replaces a character (like `\ud83c\udf69`) with its UTF-8 hex bytes (like
|
||||
* `%F0%9F%8D%A9`). This approach works for whitespace characters, control characters, non-ASCII
|
||||
* characters, and characters that already have another meaning in a particular context.
|
||||
*
|
||||
* Percent encoding is used in every URL component except for the hostname. But the set of
|
||||
* characters that need to be encoded is different for each component. For example, the path
|
||||
* component must escape all of its `?` characters, otherwise it could be interpreted as the
|
||||
* start of the URL's query. But within the query and fragment components, the `?` character
|
||||
* doesn't delimit anything and doesn't need to be escaped.
|
||||
*
|
||||
* ```java
|
||||
* HttpUrl url = HttpUrl.parse("http://who-let-the-dogs.out").newBuilder()
|
||||
* .addPathSegment("_Who?_")
|
||||
* .query("_Who?_")
|
||||
* .fragment("_Who?_")
|
||||
* .build();
|
||||
* System.out.println(url);
|
||||
* ```
|
||||
*
|
||||
* This prints:
|
||||
*
|
||||
* ```
|
||||
* http://who-let-the-dogs.out/_Who%3F_?_Who?_#_Who?_
|
||||
* ```
|
||||
*
|
||||
* When parsing URLs that lack percent encoding where it is required, this class will percent encode
|
||||
* the offending characters.
|
||||
*
|
||||
* ### IDNA Mapping and Punycode encoding
|
||||
*
|
||||
* Hostnames have different requirements and use a different encoding scheme. It consists of IDNA
|
||||
* mapping and Punycode encoding.
|
||||
*
|
||||
* In order to avoid confusion and discourage phishing attacks, [IDNA Mapping][idna] transforms
|
||||
* names to avoid confusing characters. This includes basic case folding: transforming shouting
|
||||
* `SQUARE.COM` into cool and casual `square.com`. It also handles more exotic characters. For
|
||||
* example, the Unicode trademark sign (™) could be confused for the letters "TM" in
|
||||
* `http://ho™ail.com`. To mitigate this, the single character (™) maps to the string (tm). There
|
||||
* is similar policy for all of the 1.1 million Unicode code points. Note that some code points such
|
||||
* as "\ud83c\udf69" are not mapped and cannot be used in a hostname.
|
||||
*
|
||||
* [Punycode](http://ietf.org/rfc/rfc3492.txt) converts a Unicode string to an ASCII string to make
|
||||
* international domain names work everywhere. For example, "σ" encodes as "xn--4xa". The encoded
|
||||
* string is not human readable, but can be used with classes like [InetAddress] to establish
|
||||
* connections.
|
||||
*
|
||||
* ## Why another URL model?
|
||||
*
|
||||
* Java includes both [java.net.URL][URL] and [java.net.URI][URI]. We offer a new URL
|
||||
* model to address problems that the others don't.
|
||||
*
|
||||
* ### Different URLs should be different
|
||||
*
|
||||
* Although they have different content, `java.net.URL` considers the following two URLs
|
||||
* equal, and the [equals()][Object.equals] method between them returns true:
|
||||
*
|
||||
* * https://example.net/
|
||||
*
|
||||
* * https://example.com/
|
||||
*
|
||||
* This is because those two hosts share the same IP address. This is an old, bad design decision
|
||||
* that makes `java.net.URL` unusable for many things. It shouldn't be used as a [Map] key or in a
|
||||
* [Set]. Doing so is both inefficient because equality may require a DNS lookup, and incorrect
|
||||
* because unequal URLs may be equal because of how they are hosted.
|
||||
*
|
||||
* ### Equal URLs should be equal
|
||||
*
|
||||
* These two URLs are semantically identical, but `java.net.URI` disagrees:
|
||||
*
|
||||
* * http://host:80/
|
||||
*
|
||||
* * http://host
|
||||
*
|
||||
* Both the unnecessary port specification (`:80`) and the absent trailing slash (`/`) cause URI to
|
||||
* bucket the two URLs separately. This harms URI's usefulness in collections. Any application that
|
||||
* stores information-per-URL will need to either canonicalize manually, or suffer unnecessary
|
||||
* redundancy for such URLs.
|
||||
*
|
||||
* Because they don't attempt canonical form, these classes are surprisingly difficult to use
|
||||
* securely. Suppose you're building a webservice that checks that incoming paths are prefixed
|
||||
* "/static/images/" before serving the corresponding assets from the filesystem.
|
||||
*
|
||||
* ```java
|
||||
* String attack = "http://example.com/static/images/../../../../../etc/passwd";
|
||||
* System.out.println(new URL(attack).getPath());
|
||||
* System.out.println(new URI(attack).getPath());
|
||||
* System.out.println(HttpUrl.parse(attack).encodedPath());
|
||||
* ```
|
||||
*
|
||||
* By canonicalizing the input paths, they are complicit in directory traversal attacks. Code that
|
||||
* checks only the path prefix may suffer!
|
||||
*
|
||||
* ```
|
||||
* /static/images/../../../../../etc/passwd
|
||||
* /static/images/../../../../../etc/passwd
|
||||
* /etc/passwd
|
||||
* ```
|
||||
*
|
||||
* ### If it works on the web, it should work in your application
|
||||
*
|
||||
* The `java.net.URI` class is strict around what URLs it accepts. It rejects URLs like
|
||||
* `http://example.com/abc|def` because the `|` character is unsupported. This class is more
|
||||
* forgiving: it will automatically percent-encode the `|'` yielding `http://example.com/abc%7Cdef`.
|
||||
* This kind behavior is consistent with web browsers. `HttpUrl` prefers consistency with major web
|
||||
* browsers over consistency with obsolete specifications.
|
||||
*
|
||||
* ### Paths and Queries should decompose
|
||||
*
|
||||
* Neither of the built-in URL models offer direct access to path segments or query parameters.
|
||||
* Manually using `StringBuilder` to assemble these components is cumbersome: do '+' characters get
|
||||
* silently replaced with spaces? If a query parameter contains a '&', does that get escaped?
|
||||
* By offering methods to read and write individual query parameters directly, application
|
||||
* developers are saved from the hassles of encoding and decoding.
|
||||
*
|
||||
* ### Plus a modern API
|
||||
*
|
||||
* The URL (JDK1.0) and URI (Java 1.4) classes predate builders and instead use telescoping
|
||||
* constructors. For example, there's no API to compose a URI with a custom port without also
|
||||
* providing a query and fragment.
|
||||
*
|
||||
* Instances of [HttpUrl] are well-formed and always have a scheme, host, and path. With
|
||||
* `java.net.URL` it's possible to create an awkward URL like `http:/` with scheme and path but no
|
||||
* hostname. Building APIs that consume such malformed values is difficult!
|
||||
*
|
||||
* This class has a modern API. It avoids punitive checked exceptions: [toHttpUrl] throws
|
||||
* [IllegalArgumentException] on invalid input or [toHttpUrlOrNull] returns null if the input is an
|
||||
* invalid URL. You can even be explicit about whether each component has been encoded already.
|
||||
*
|
||||
* [idna]: http://www.unicode.org/reports/tr46/#ToASCII
|
||||
*/
|
||||
expect class HttpUrl internal constructor(
|
||||
scheme: String,
|
||||
username: String,
|
||||
password: String,
|
||||
host: String,
|
||||
port: Int,
|
||||
pathSegments: List<String>,
|
||||
queryNamesAndValues: List<String?>?,
|
||||
fragment: String?,
|
||||
url: String
|
||||
) {
|
||||
|
||||
/** Either "http" or "https". */
|
||||
val scheme: String
|
||||
|
||||
/**
|
||||
* The decoded username, or an empty string if none is present.
|
||||
*
|
||||
* | URL | `username()` |
|
||||
* | :------------------------------- | :----------- |
|
||||
* | `http://host/` | `""` |
|
||||
* | `http://username@host/` | `"username"` |
|
||||
* | `http://username:password@host/` | `"username"` |
|
||||
* | `http://a%20b:c%20d@host/` | `"a b"` |
|
||||
*/
|
||||
val username: String
|
||||
|
||||
/**
|
||||
* Returns the decoded password, or an empty string if none is present.
|
||||
*
|
||||
* | URL | `password()` |
|
||||
* | :------------------------------- | :----------- |
|
||||
* | `http://host/` | `""` |
|
||||
* | `http://username@host/` | `""` |
|
||||
* | `http://username:password@host/` | `"password"` |
|
||||
* | `http://a%20b:c%20d@host/` | `"c d"` |
|
||||
*/
|
||||
val password: String
|
||||
|
||||
/**
|
||||
* The host address suitable for use with [InetAddress.getAllByName]. May be:
|
||||
*
|
||||
* * A regular host name, like `android.com`.
|
||||
*
|
||||
* * An IPv4 address, like `127.0.0.1`.
|
||||
*
|
||||
* * An IPv6 address, like `::1`. Note that there are no square braces.
|
||||
*
|
||||
* * An encoded IDN, like `xn--n3h.net`.
|
||||
*
|
||||
* | URL | `host()` |
|
||||
* | :-------------------- | :-------------- |
|
||||
* | `http://android.com/` | `"android.com"` |
|
||||
* | `http://127.0.0.1/` | `"127.0.0.1"` |
|
||||
* | `http://[::1]/` | `"::1"` |
|
||||
* | `http://xn--n3h.net/` | `"xn--n3h.net"` |
|
||||
*/
|
||||
val host: String
|
||||
|
||||
/**
|
||||
* The explicitly-specified port if one was provided, or the default port for this URL's scheme.
|
||||
* For example, this returns 8443 for `https://square.com:8443/` and 443 for
|
||||
* `https://square.com/`. The result is in `[1..65535]`.
|
||||
*
|
||||
* | URL | `port()` |
|
||||
* | :------------------ | :------- |
|
||||
* | `http://host/` | `80` |
|
||||
* | `http://host:8000/` | `8000` |
|
||||
* | `https://host/` | `443` |
|
||||
*/
|
||||
val port: Int
|
||||
|
||||
/**
|
||||
* A list of path segments like `["a", "b", "c"]` for the URL `http://host/a/b/c`. This list is
|
||||
* never empty though it may contain a single empty string.
|
||||
*
|
||||
* | URL | `pathSegments()` |
|
||||
* | :----------------------- | :------------------ |
|
||||
* | `http://host/` | `[""]` |
|
||||
* | `http://host/a/b/c"` | `["a", "b", "c"]` |
|
||||
* | `http://host/a/b%20c/d"` | `["a", "b c", "d"]` |
|
||||
*/
|
||||
val pathSegments: List<String>
|
||||
|
||||
/**
|
||||
* This URL's fragment, like `"abc"` for `http://host/#abc`. This is null if the URL has no
|
||||
* fragment.
|
||||
*
|
||||
* | URL | `fragment()` |
|
||||
* | :--------------------- | :----------- |
|
||||
* | `http://host/` | null |
|
||||
* | `http://host/#` | `""` |
|
||||
* | `http://host/#abc` | `"abc"` |
|
||||
* | `http://host/#abc|def` | `"abc|def"` |
|
||||
*/
|
||||
val fragment: String?
|
||||
|
||||
val isHttps: Boolean
|
||||
|
||||
/**
|
||||
* The username, or an empty string if none is set.
|
||||
*
|
||||
* | URL | `encodedUsername()` |
|
||||
* | :------------------------------- | :------------------ |
|
||||
* | `http://host/` | `""` |
|
||||
* | `http://username@host/` | `"username"` |
|
||||
* | `http://username:password@host/` | `"username"` |
|
||||
* | `http://a%20b:c%20d@host/` | `"a%20b"` |
|
||||
*/
|
||||
val encodedUsername: String
|
||||
|
||||
/**
|
||||
* The password, or an empty string if none is set.
|
||||
*
|
||||
* | URL | `encodedPassword()` |
|
||||
* | :--------------------------------| :------------------ |
|
||||
* | `http://host/` | `""` |
|
||||
* | `http://username@host/` | `""` |
|
||||
* | `http://username:password@host/` | `"password"` |
|
||||
* | `http://a%20b:c%20d@host/` | `"c%20d"` |
|
||||
*/
|
||||
val encodedPassword: String
|
||||
|
||||
/**
|
||||
* The number of segments in this URL's path. This is also the number of slashes in this URL's
|
||||
* path, like 3 in `http://host/a/b/c`. This is always at least 1.
|
||||
*
|
||||
* | URL | `pathSize()` |
|
||||
* | :------------------- | :----------- |
|
||||
* | `http://host/` | `1` |
|
||||
* | `http://host/a/b/c` | `3` |
|
||||
* | `http://host/a/b/c/` | `4` |
|
||||
*/
|
||||
val pathSize: Int
|
||||
|
||||
/**
|
||||
* The entire path of this URL encoded for use in HTTP resource resolution. The returned path will
|
||||
* start with `"/"`.
|
||||
*
|
||||
* | URL | `encodedPath()` |
|
||||
* | :---------------------- | :-------------- |
|
||||
* | `http://host/` | `"/"` |
|
||||
* | `http://host/a/b/c` | `"/a/b/c"` |
|
||||
* | `http://host/a/b%20c/d` | `"/a/b%20c/d"` |
|
||||
*/
|
||||
val encodedPath: String
|
||||
|
||||
/**
|
||||
* A list of encoded path segments like `["a", "b", "c"]` for the URL `http://host/a/b/c`. This
|
||||
* list is never empty though it may contain a single empty string.
|
||||
*
|
||||
* | URL | `encodedPathSegments()` |
|
||||
* | :---------------------- | :---------------------- |
|
||||
* | `http://host/` | `[""]` |
|
||||
* | `http://host/a/b/c` | `["a", "b", "c"]` |
|
||||
* | `http://host/a/b%20c/d` | `["a", "b%20c", "d"]` |
|
||||
*/
|
||||
val encodedPathSegments: List<String>
|
||||
|
||||
/**
|
||||
* The query of this URL, encoded for use in HTTP resource resolution. This string may be null
|
||||
* (for URLs with no query), empty (for URLs with an empty query) or non-empty (all other URLs).
|
||||
*
|
||||
* | URL | `encodedQuery()` |
|
||||
* | :-------------------------------- | :--------------------- |
|
||||
* | `http://host/` | null |
|
||||
* | `http://host/?` | `""` |
|
||||
* | `http://host/?a=apple&k=key+lime` | `"a=apple&k=key+lime"` |
|
||||
* | `http://host/?a=apple&a=apricot` | `"a=apple&a=apricot"` |
|
||||
* | `http://host/?a=apple&b` | `"a=apple&b"` |
|
||||
*/
|
||||
val encodedQuery: String?
|
||||
|
||||
/**
|
||||
* This URL's query, like `"abc"` for `http://host/?abc`. Most callers should prefer
|
||||
* [queryParameterName] and [queryParameterValue] because these methods offer direct access to
|
||||
* individual query parameters.
|
||||
*
|
||||
* | URL | `query()` |
|
||||
* | :-------------------------------- | :--------------------- |
|
||||
* | `http://host/` | null |
|
||||
* | `http://host/?` | `""` |
|
||||
* | `http://host/?a=apple&k=key+lime` | `"a=apple&k=key lime"` |
|
||||
* | `http://host/?a=apple&a=apricot` | `"a=apple&a=apricot"` |
|
||||
* | `http://host/?a=apple&b` | `"a=apple&b"` |
|
||||
*/
|
||||
val query: String?
|
||||
|
||||
/**
|
||||
* The number of query parameters in this URL, like 2 for `http://host/?a=apple&b=banana`. If this
|
||||
* URL has no query this is 0. Otherwise it is one more than the number of `"&"` separators in the
|
||||
* query.
|
||||
*
|
||||
* | URL | `querySize()` |
|
||||
* | :-------------------------------- | :------------ |
|
||||
* | `http://host/` | `0` |
|
||||
* | `http://host/?` | `1` |
|
||||
* | `http://host/?a=apple&k=key+lime` | `2` |
|
||||
* | `http://host/?a=apple&a=apricot` | `2` |
|
||||
* | `http://host/?a=apple&b` | `2` |
|
||||
*/
|
||||
val querySize: Int
|
||||
|
||||
/**
|
||||
* The first query parameter named `name` decoded using UTF-8, or null if there is no such query
|
||||
* parameter.
|
||||
*
|
||||
* | URL | `queryParameter("a")` |
|
||||
* | :-------------------------------- | :-------------------- |
|
||||
* | `http://host/` | null |
|
||||
* | `http://host/?` | null |
|
||||
* | `http://host/?a=apple&k=key+lime` | `"apple"` |
|
||||
* | `http://host/?a=apple&a=apricot` | `"apple"` |
|
||||
* | `http://host/?a=apple&b` | `"apple"` |
|
||||
*/
|
||||
fun queryParameter(name: String): String?
|
||||
|
||||
/**
|
||||
* The distinct query parameter names in this URL, like `["a", "b"]` for
|
||||
* `http://host/?a=apple&b=banana`. If this URL has no query this is the empty set.
|
||||
*
|
||||
* | URL | `queryParameterNames()` |
|
||||
* | :-------------------------------- | :---------------------- |
|
||||
* | `http://host/` | `[]` |
|
||||
* | `http://host/?` | `[""]` |
|
||||
* | `http://host/?a=apple&k=key+lime` | `["a", "k"]` |
|
||||
* | `http://host/?a=apple&a=apricot` | `["a"]` |
|
||||
* | `http://host/?a=apple&b` | `["a", "b"]` |
|
||||
*/
|
||||
val queryParameterNames: Set<String>
|
||||
|
||||
internal val url: String
|
||||
|
||||
internal val queryNamesAndValues: List<String?>?
|
||||
|
||||
/**
|
||||
* Returns all values for the query parameter `name` ordered by their appearance in this
|
||||
* URL. For example this returns `["banana"]` for `queryParameterValue("b")` on
|
||||
* `http://host/?a=apple&b=banana`.
|
||||
*
|
||||
* | URL | `queryParameterValues("a")` | `queryParameterValues("b")` |
|
||||
* | :-------------------------------- | :-------------------------- | :-------------------------- |
|
||||
* | `http://host/` | `[]` | `[]` |
|
||||
* | `http://host/?` | `[]` | `[]` |
|
||||
* | `http://host/?a=apple&k=key+lime` | `["apple"]` | `[]` |
|
||||
* | `http://host/?a=apple&a=apricot` | `["apple", "apricot"]` | `[]` |
|
||||
* | `http://host/?a=apple&b` | `["apple"]` | `[null]` |
|
||||
*/
|
||||
fun queryParameterValues(name: String): List<String?>
|
||||
|
||||
/**
|
||||
* Returns the name of the query parameter at `index`. For example this returns `"a"`
|
||||
* for `queryParameterName(0)` on `http://host/?a=apple&b=banana`. This throws if
|
||||
* `index` is not less than the [query size][querySize].
|
||||
*
|
||||
* | URL | `queryParameterName(0)` | `queryParameterName(1)` |
|
||||
* | :-------------------------------- | :---------------------- | :---------------------- |
|
||||
* | `http://host/` | exception | exception |
|
||||
* | `http://host/?` | `""` | exception |
|
||||
* | `http://host/?a=apple&k=key+lime` | `"a"` | `"k"` |
|
||||
* | `http://host/?a=apple&a=apricot` | `"a"` | `"a"` |
|
||||
* | `http://host/?a=apple&b` | `"a"` | `"b"` |
|
||||
*/
|
||||
fun queryParameterName(index: Int): String
|
||||
|
||||
/**
|
||||
* Returns the value of the query parameter at `index`. For example this returns `"apple"` for
|
||||
* `queryParameterName(0)` on `http://host/?a=apple&b=banana`. This throws if `index` is not less
|
||||
* than the [query size][querySize].
|
||||
*
|
||||
* | URL | `queryParameterValue(0)` | `queryParameterValue(1)` |
|
||||
* | :-------------------------------- | :----------------------- | :----------------------- |
|
||||
* | `http://host/` | exception | exception |
|
||||
* | `http://host/?` | null | exception |
|
||||
* | `http://host/?a=apple&k=key+lime` | `"apple"` | `"key lime"` |
|
||||
* | `http://host/?a=apple&a=apricot` | `"apple"` | `"apricot"` |
|
||||
* | `http://host/?a=apple&b` | `"apple"` | null |
|
||||
*/
|
||||
fun queryParameterValue(index: Int): String?
|
||||
|
||||
/**
|
||||
* This URL's encoded fragment, like `"abc"` for `http://host/#abc`. This is null if the URL has
|
||||
* no fragment.
|
||||
*
|
||||
* | URL | `encodedFragment()` |
|
||||
* | :--------------------- | :------------------ |
|
||||
* | `http://host/` | null |
|
||||
* | `http://host/#` | `""` |
|
||||
* | `http://host/#abc` | `"abc"` |
|
||||
* | `http://host/#abc|def` | `"abc|def"` |
|
||||
*/
|
||||
@get:JvmName("encodedFragment") val encodedFragment: String?
|
||||
|
||||
/**
|
||||
* Returns a string with containing this URL with its username, password, query, and fragment
|
||||
* stripped, and its path replaced with `/...`. For example, redacting
|
||||
* `http://username:password@example.com/path` returns `http://example.com/...`.
|
||||
*/
|
||||
fun redact(): String
|
||||
|
||||
/**
|
||||
* Returns the URL that would be retrieved by following `link` from this URL, or null if the
|
||||
* resulting URL is not well-formed.
|
||||
*/
|
||||
fun resolve(link: String): HttpUrl?
|
||||
|
||||
/**
|
||||
* Returns a builder based on this URL.
|
||||
*/
|
||||
fun newBuilder(): Builder
|
||||
|
||||
/**
|
||||
* Returns a builder for the URL that would be retrieved by following `link` from this URL,
|
||||
* or null if the resulting URL is not well-formed.
|
||||
*/
|
||||
fun newBuilder(link: String): Builder?
|
||||
|
||||
class Builder constructor() {
|
||||
internal var scheme: String?
|
||||
internal var encodedUsername: String
|
||||
internal var encodedPassword: String
|
||||
internal var host: String?
|
||||
internal var port: Int
|
||||
internal val encodedPathSegments: MutableList<String>
|
||||
internal var encodedQueryNamesAndValues: MutableList<String?>?
|
||||
internal var encodedFragment: String?
|
||||
|
||||
fun scheme(scheme: String): Builder
|
||||
fun username(username: String): Builder
|
||||
|
||||
fun encodedUsername(encodedUsername: String): Builder
|
||||
|
||||
fun password(password: String): Builder
|
||||
|
||||
fun encodedPassword(encodedPassword: String): Builder
|
||||
|
||||
fun host(host: String): Builder
|
||||
fun port(port: Int): Builder
|
||||
|
||||
fun addPathSegment(pathSegment: String): Builder
|
||||
|
||||
/**
|
||||
* Adds a set of path segments separated by a slash (either `\` or `/`). If `pathSegments`
|
||||
* starts with a slash, the resulting URL will have empty path segment.
|
||||
*/
|
||||
fun addPathSegments(pathSegments: String): Builder
|
||||
|
||||
fun addEncodedPathSegment(encodedPathSegment: String): Builder
|
||||
|
||||
/**
|
||||
* Adds a set of encoded path segments separated by a slash (either `\` or `/`). If
|
||||
* `encodedPathSegments` starts with a slash, the resulting URL will have empty path segment.
|
||||
*/
|
||||
fun addEncodedPathSegments(encodedPathSegments: String): Builder
|
||||
|
||||
fun setPathSegment(index: Int, pathSegment: String): Builder
|
||||
|
||||
fun setEncodedPathSegment(index: Int, encodedPathSegment: String): Builder
|
||||
|
||||
fun removePathSegment(index: Int): Builder
|
||||
|
||||
fun encodedPath(encodedPath: String): Builder
|
||||
|
||||
fun query(query: String?): Builder
|
||||
|
||||
fun encodedQuery(encodedQuery: String?): Builder
|
||||
|
||||
/** Encodes the query parameter using UTF-8 and adds it to this URL's query string. */
|
||||
fun addQueryParameter(name: String, value: String?): Builder
|
||||
|
||||
/** Adds the pre-encoded query parameter to this URL's query string. */
|
||||
fun addEncodedQueryParameter(encodedName: String, encodedValue: String?): Builder
|
||||
|
||||
fun setQueryParameter(name: String, value: String?): Builder
|
||||
|
||||
fun setEncodedQueryParameter(encodedName: String, encodedValue: String?): Builder
|
||||
|
||||
fun removeAllQueryParameters(name: String): Builder
|
||||
|
||||
fun removeAllEncodedQueryParameters(encodedName: String): Builder
|
||||
|
||||
fun fragment(fragment: String?): Builder
|
||||
|
||||
fun encodedFragment(encodedFragment: String?): Builder
|
||||
|
||||
fun build(): HttpUrl
|
||||
|
||||
internal fun parse(base: HttpUrl?, input: String): Builder
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun defaultPort(scheme: String): Int
|
||||
|
||||
/**
|
||||
* Returns a new [HttpUrl] representing this.
|
||||
*
|
||||
* @throws IllegalArgumentException If this is not a well-formed HTTP or HTTPS URL.
|
||||
*/
|
||||
fun String.toHttpUrl(): HttpUrl
|
||||
|
||||
/**
|
||||
* Returns a new `HttpUrl` representing `url` if it is a well-formed HTTP or HTTPS URL, or null
|
||||
* if it isn't.
|
||||
*/
|
||||
fun String.toHttpUrlOrNull(): HttpUrl?
|
||||
}
|
||||
}
|
@@ -16,7 +16,6 @@
|
||||
package okhttp3
|
||||
|
||||
import kotlin.reflect.KClass
|
||||
import okhttp3.internal.HttpUrlRepresentation
|
||||
import okhttp3.internal.commonEmptyRequestBody
|
||||
|
||||
/**
|
||||
@@ -24,7 +23,7 @@ import okhttp3.internal.commonEmptyRequestBody
|
||||
* immutable.
|
||||
*/
|
||||
expect class Request private constructor(builder: Builder) {
|
||||
val url: HttpUrlRepresentation
|
||||
val url: HttpUrl
|
||||
val method: String
|
||||
val headers: Headers
|
||||
val body: RequestBody?
|
||||
@@ -53,7 +52,7 @@ expect class Request private constructor(builder: Builder) {
|
||||
val cacheControl: CacheControl
|
||||
|
||||
open class Builder {
|
||||
internal var url: HttpUrlRepresentation?
|
||||
internal var url: HttpUrl?
|
||||
internal var method: String
|
||||
internal var headers: Headers.Builder
|
||||
internal var body: RequestBody?
|
||||
@@ -68,8 +67,7 @@ expect class Request private constructor(builder: Builder) {
|
||||
// /** A mutable map of tags, or an immutable empty map if we don't have any. */
|
||||
// internal var tags: MutableMap<Class<*>, Any> = mutableMapOf()
|
||||
|
||||
// Wait for HttpUrl
|
||||
// open fun url(url: HttpUrl): Builder
|
||||
open fun url(url: HttpUrl): Builder
|
||||
|
||||
/**
|
||||
* Sets the URL target of this request.
|
||||
|
@@ -224,3 +224,5 @@ internal fun inet6AddressToAscii(address: ByteArray): String {
|
||||
}
|
||||
return result.readUtf8()
|
||||
}
|
||||
|
||||
expect fun String.toCanonicalHost(): String?
|
||||
|
904
okhttp/src/commonMain/kotlin/okhttp3/internal/-HttpUrlCommon.kt
Normal file
904
okhttp/src/commonMain/kotlin/okhttp3/internal/-HttpUrlCommon.kt
Normal file
@@ -0,0 +1,904 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package okhttp3.internal
|
||||
|
||||
import kotlin.jvm.JvmStatic
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.internal.HttpUrlCommon.canonicalize
|
||||
import okhttp3.internal.HttpUrlCommon.writePercentDecoded
|
||||
import okio.Buffer
|
||||
|
||||
internal expect object HttpUrlCommon {
|
||||
internal fun Buffer.writePercentDecoded(
|
||||
encoded: String,
|
||||
pos: Int,
|
||||
limit: Int,
|
||||
plusIsSpace: Boolean
|
||||
)
|
||||
|
||||
internal fun String.canonicalize(
|
||||
pos: Int = 0,
|
||||
limit: Int = length,
|
||||
encodeSet: String,
|
||||
alreadyEncoded: Boolean = false,
|
||||
strict: Boolean = false,
|
||||
plusIsSpace: Boolean = false,
|
||||
unicodeAllowed: Boolean = false,
|
||||
): String
|
||||
|
||||
}
|
||||
|
||||
internal object CommonHttpUrl {
|
||||
|
||||
internal val HEX_DIGITS =
|
||||
charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F')
|
||||
private const val USERNAME_ENCODE_SET = " \"':;<=>@[]^`{}|/\\?#"
|
||||
private const val PASSWORD_ENCODE_SET = " \"':;<=>@[]^`{}|/\\?#"
|
||||
private const val PATH_SEGMENT_ENCODE_SET = " \"<>^`{}|/\\?#"
|
||||
internal const val PATH_SEGMENT_ENCODE_SET_URI = "[]"
|
||||
private const val QUERY_ENCODE_SET = " \"'<>#"
|
||||
private const val QUERY_COMPONENT_REENCODE_SET = " \"'<>#&="
|
||||
private const val QUERY_COMPONENT_ENCODE_SET = " !\"#$&'(),/:;<=>?@[]\\^`{|}~"
|
||||
internal const val QUERY_COMPONENT_ENCODE_SET_URI = "\\^`{|}"
|
||||
internal const val FORM_ENCODE_SET = " !\"#$&'()+,/:;<=>?@[\\]^`{|}~"
|
||||
private const val FRAGMENT_ENCODE_SET = ""
|
||||
internal const val FRAGMENT_ENCODE_SET_URI = " \"#<>\\^`{|}"
|
||||
|
||||
val HttpUrl.commonIsHttps: Boolean
|
||||
get() = scheme == "https"
|
||||
|
||||
val HttpUrl.commonEncodedUsername: String
|
||||
get() {
|
||||
if (username.isEmpty()) return ""
|
||||
val usernameStart = scheme.length + 3 // "://".length() == 3.
|
||||
val usernameEnd = url.delimiterOffset(":@", usernameStart, url.length)
|
||||
return url.substring(usernameStart, usernameEnd)
|
||||
}
|
||||
|
||||
val HttpUrl.commonEncodedPassword: String
|
||||
get() {
|
||||
if (password.isEmpty()) return ""
|
||||
val passwordStart = url.indexOf(':', scheme.length + 3) + 1
|
||||
val passwordEnd = url.indexOf('@')
|
||||
return url.substring(passwordStart, passwordEnd)
|
||||
}
|
||||
|
||||
val HttpUrl.commonPathSize: Int get() = pathSegments.size
|
||||
|
||||
val HttpUrl.commonEncodedPath: String
|
||||
get() {
|
||||
val pathStart = url.indexOf('/', scheme.length + 3) // "://".length() == 3.
|
||||
val pathEnd = url.delimiterOffset("?#", pathStart, url.length)
|
||||
return url.substring(pathStart, pathEnd)
|
||||
}
|
||||
|
||||
val HttpUrl.commonEncodedPathSegments: List<String>
|
||||
get() {
|
||||
val pathStart = url.indexOf('/', scheme.length + 3)
|
||||
val pathEnd = url.delimiterOffset("?#", pathStart, url.length)
|
||||
val result = mutableListOf<String>()
|
||||
var i = pathStart
|
||||
while (i < pathEnd) {
|
||||
i++ // Skip the '/'.
|
||||
val segmentEnd = url.delimiterOffset('/', i, pathEnd)
|
||||
result.add(url.substring(i, segmentEnd))
|
||||
i = segmentEnd
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
val HttpUrl.commonEncodedQuery: String?
|
||||
get() {
|
||||
if (queryNamesAndValues == null) return null // No query.
|
||||
val queryStart = url.indexOf('?') + 1
|
||||
val queryEnd = url.delimiterOffset('#', queryStart, url.length)
|
||||
return url.substring(queryStart, queryEnd)
|
||||
}
|
||||
|
||||
val HttpUrl.commonQuery: String?
|
||||
get() {
|
||||
if (queryNamesAndValues == null) return null // No query.
|
||||
val result = StringBuilder()
|
||||
queryNamesAndValues.toQueryString(result)
|
||||
return result.toString()
|
||||
}
|
||||
|
||||
val HttpUrl.commonQuerySize: Int
|
||||
get() {
|
||||
return if (queryNamesAndValues != null) queryNamesAndValues.size / 2 else 0
|
||||
}
|
||||
|
||||
fun HttpUrl.commonQueryParameter(name: String): String? {
|
||||
if (queryNamesAndValues == null) return null
|
||||
for (i in 0 until queryNamesAndValues.size step 2) {
|
||||
if (name == queryNamesAndValues[i]) {
|
||||
return queryNamesAndValues[i + 1]
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
val HttpUrl.commonQueryParameterNames: Set<String>
|
||||
get() {
|
||||
if (queryNamesAndValues == null) return emptySet()
|
||||
val result = LinkedHashSet<String>()
|
||||
for (i in 0 until queryNamesAndValues.size step 2) {
|
||||
result.add(queryNamesAndValues[i]!!)
|
||||
}
|
||||
return result.readOnly()
|
||||
}
|
||||
|
||||
fun HttpUrl.commonQueryParameterValues(name: String): List<String?> {
|
||||
if (queryNamesAndValues == null) return emptyList()
|
||||
val result = mutableListOf<String?>()
|
||||
for (i in 0 until queryNamesAndValues.size step 2) {
|
||||
if (name == queryNamesAndValues[i]) {
|
||||
result.add(queryNamesAndValues[i + 1])
|
||||
}
|
||||
}
|
||||
return result.readOnly()
|
||||
}
|
||||
|
||||
fun HttpUrl.commonQueryParameterName(index: Int): String {
|
||||
if (queryNamesAndValues == null) throw IndexOutOfBoundsException()
|
||||
return queryNamesAndValues[index * 2]!!
|
||||
}
|
||||
|
||||
fun HttpUrl.commonQueryParameterValue(index: Int): String? {
|
||||
if (queryNamesAndValues == null) throw IndexOutOfBoundsException()
|
||||
return queryNamesAndValues[index * 2 + 1]
|
||||
}
|
||||
|
||||
val HttpUrl.commonEncodedFragment: String?
|
||||
get() {
|
||||
if (fragment == null) return null
|
||||
val fragmentStart = url.indexOf('#') + 1
|
||||
return url.substring(fragmentStart)
|
||||
}
|
||||
|
||||
/** Returns a string for this list of query names and values. */
|
||||
internal fun List<String?>.toQueryString(out: StringBuilder) {
|
||||
for (i in 0 until size step 2) {
|
||||
val name = this[i]
|
||||
val value = this[i + 1]
|
||||
if (i > 0) out.append('&')
|
||||
out.append(name)
|
||||
if (value != null) {
|
||||
out.append('=')
|
||||
out.append(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun HttpUrl.commonRedact(): String {
|
||||
return newBuilder("/...")!!
|
||||
.username("")
|
||||
.password("")
|
||||
.build()
|
||||
.toString()
|
||||
}
|
||||
|
||||
|
||||
fun HttpUrl.commonResolve(link: String): HttpUrl? = newBuilder(link)?.build()
|
||||
|
||||
fun HttpUrl.commonNewBuilder(): HttpUrl.Builder {
|
||||
val result = HttpUrl.Builder()
|
||||
result.scheme = scheme
|
||||
result.encodedUsername = encodedUsername
|
||||
result.encodedPassword = encodedPassword
|
||||
result.host = host
|
||||
// If we're set to a default port, unset it in case of a scheme change.
|
||||
result.port = if (port != commonDefaultPort(scheme)) port else -1
|
||||
result.encodedPathSegments.clear()
|
||||
result.encodedPathSegments.addAll(encodedPathSegments)
|
||||
result.encodedQuery(encodedQuery)
|
||||
result.encodedFragment = encodedFragment
|
||||
return result
|
||||
}
|
||||
|
||||
fun HttpUrl.commonNewBuilder(link: String): HttpUrl.Builder? {
|
||||
return try {
|
||||
HttpUrl.Builder().parse(this, link)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun HttpUrl.commonEquals(other: Any?): Boolean {
|
||||
return other is HttpUrl && other.url == url
|
||||
}
|
||||
|
||||
fun HttpUrl.commonHashCode(): Int = url.hashCode()
|
||||
|
||||
fun HttpUrl.commonToString(): String = url
|
||||
|
||||
|
||||
/** Returns 80 if `scheme.equals("http")`, 443 if `scheme.equals("https")` and -1 otherwise. */
|
||||
@JvmStatic
|
||||
fun commonDefaultPort(scheme: String): Int {
|
||||
return when (scheme) {
|
||||
"http" -> 80
|
||||
"https" -> 443
|
||||
else -> -1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param scheme either "http" or "https".
|
||||
*/
|
||||
fun HttpUrl.Builder.commonScheme(scheme: String) = apply {
|
||||
when {
|
||||
scheme.equals("http", ignoreCase = true) -> this.scheme = "http"
|
||||
scheme.equals("https", ignoreCase = true) -> this.scheme = "https"
|
||||
else -> throw IllegalArgumentException("unexpected scheme: $scheme")
|
||||
}
|
||||
}
|
||||
|
||||
fun HttpUrl.Builder.commonUsername(username: String) = apply {
|
||||
this.encodedUsername = username.canonicalize(encodeSet = USERNAME_ENCODE_SET)
|
||||
}
|
||||
|
||||
fun HttpUrl.Builder.commonEncodedUsername(encodedUsername: String) = apply {
|
||||
this.encodedUsername = encodedUsername.canonicalize(
|
||||
encodeSet = USERNAME_ENCODE_SET,
|
||||
alreadyEncoded = true
|
||||
)
|
||||
}
|
||||
|
||||
fun HttpUrl.Builder.commonPassword(password: String) = apply {
|
||||
this.encodedPassword = password.canonicalize(encodeSet = PASSWORD_ENCODE_SET)
|
||||
}
|
||||
|
||||
fun HttpUrl.Builder.commonEncodedPassword(encodedPassword: String) = apply {
|
||||
this.encodedPassword = encodedPassword.canonicalize(
|
||||
encodeSet = PASSWORD_ENCODE_SET,
|
||||
alreadyEncoded = true
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param host either a regular hostname, International Domain Name, IPv4 address, or IPv6
|
||||
* address.
|
||||
*/
|
||||
fun HttpUrl.Builder.commonHost(host: String) = apply {
|
||||
val encoded = host.percentDecode().toCanonicalHost() ?: throw IllegalArgumentException(
|
||||
"unexpected host: $host")
|
||||
this.host = encoded
|
||||
}
|
||||
|
||||
internal fun String.percentDecode(
|
||||
pos: Int = 0,
|
||||
limit: Int = length,
|
||||
plusIsSpace: Boolean = false
|
||||
): String {
|
||||
for (i in pos until limit) {
|
||||
val c = this[i]
|
||||
if (c == '%' || c == '+' && plusIsSpace) {
|
||||
// Slow path: the character at i requires decoding!
|
||||
val out = Buffer()
|
||||
out.writeUtf8(this, pos, i)
|
||||
out.writePercentDecoded(this, pos = i, limit = limit, plusIsSpace = plusIsSpace)
|
||||
return out.readUtf8()
|
||||
}
|
||||
}
|
||||
|
||||
// Fast path: no characters in [pos..limit) required decoding.
|
||||
return substring(pos, limit)
|
||||
}
|
||||
|
||||
fun HttpUrl.Builder.commonPort(port: Int) = apply {
|
||||
require(port in 1..65535) { "unexpected port: $port" }
|
||||
this.port = port
|
||||
}
|
||||
|
||||
|
||||
fun HttpUrl.Builder.commonAddPathSegment(pathSegment: String) = apply {
|
||||
push(pathSegment, 0, pathSegment.length, addTrailingSlash = false, alreadyEncoded = false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a set of path segments separated by a slash (either `\` or `/`). If `pathSegments`
|
||||
* starts with a slash, the resulting URL will have empty path segment.
|
||||
*/
|
||||
fun HttpUrl.Builder.commonAddPathSegments(pathSegments: String): HttpUrl.Builder = commonAddPathSegments(pathSegments, false)
|
||||
|
||||
fun HttpUrl.Builder.commonAddEncodedPathSegment(encodedPathSegment: String) = apply {
|
||||
push(encodedPathSegment, 0, encodedPathSegment.length, addTrailingSlash = false,
|
||||
alreadyEncoded = true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a set of encoded path segments separated by a slash (either `\` or `/`). If
|
||||
* `encodedPathSegments` starts with a slash, the resulting URL will have empty path segment.
|
||||
*/
|
||||
fun HttpUrl.Builder.commonAddEncodedPathSegments(encodedPathSegments: String): HttpUrl.Builder =
|
||||
commonAddPathSegments(encodedPathSegments, true)
|
||||
|
||||
private fun HttpUrl.Builder.commonAddPathSegments(pathSegments: String, alreadyEncoded: Boolean) = apply {
|
||||
var offset = 0
|
||||
do {
|
||||
val segmentEnd = pathSegments.delimiterOffset("/\\", offset, pathSegments.length)
|
||||
val addTrailingSlash = segmentEnd < pathSegments.length
|
||||
push(pathSegments, offset, segmentEnd, addTrailingSlash, alreadyEncoded)
|
||||
offset = segmentEnd + 1
|
||||
} while (offset <= pathSegments.length)
|
||||
}
|
||||
|
||||
fun HttpUrl.Builder.commonSetPathSegment(index: Int, pathSegment: String) = apply {
|
||||
val canonicalPathSegment = pathSegment.canonicalize(encodeSet = PATH_SEGMENT_ENCODE_SET)
|
||||
require(!isDot(canonicalPathSegment) && !isDotDot(canonicalPathSegment)) {
|
||||
"unexpected path segment: $pathSegment"
|
||||
}
|
||||
encodedPathSegments[index] = canonicalPathSegment
|
||||
}
|
||||
|
||||
fun HttpUrl.Builder.commonSetEncodedPathSegment(index: Int, encodedPathSegment: String) = apply {
|
||||
val canonicalPathSegment = encodedPathSegment.canonicalize(
|
||||
encodeSet = PATH_SEGMENT_ENCODE_SET,
|
||||
alreadyEncoded = true
|
||||
)
|
||||
encodedPathSegments[index] = canonicalPathSegment
|
||||
require(!isDot(canonicalPathSegment) && !isDotDot(canonicalPathSegment)) {
|
||||
"unexpected path segment: $encodedPathSegment"
|
||||
}
|
||||
}
|
||||
|
||||
fun HttpUrl.Builder.commonRemovePathSegment(index: Int) = apply {
|
||||
encodedPathSegments.removeAt(index)
|
||||
if (encodedPathSegments.isEmpty()) {
|
||||
encodedPathSegments.add("") // Always leave at least one '/'.
|
||||
}
|
||||
}
|
||||
|
||||
fun HttpUrl.Builder.commonEncodedPath(encodedPath: String) = apply {
|
||||
require(encodedPath.startsWith("/")) { "unexpected encodedPath: $encodedPath" }
|
||||
resolvePath(encodedPath, 0, encodedPath.length)
|
||||
}
|
||||
|
||||
fun HttpUrl.Builder.commonQuery(query: String?) = apply {
|
||||
this.encodedQueryNamesAndValues = query?.canonicalize(
|
||||
encodeSet = QUERY_ENCODE_SET,
|
||||
plusIsSpace = true
|
||||
)?.toQueryNamesAndValues()
|
||||
}
|
||||
|
||||
fun HttpUrl.Builder.commonEncodedQuery(encodedQuery: String?) = apply {
|
||||
this.encodedQueryNamesAndValues = encodedQuery?.canonicalize(
|
||||
encodeSet = QUERY_ENCODE_SET,
|
||||
alreadyEncoded = true,
|
||||
plusIsSpace = true
|
||||
)?.toQueryNamesAndValues()
|
||||
}
|
||||
|
||||
/** Encodes the query parameter using UTF-8 and adds it to this URL's query string. */
|
||||
fun HttpUrl.Builder.commonAddQueryParameter(name: String, value: String?) = apply {
|
||||
if (encodedQueryNamesAndValues == null) encodedQueryNamesAndValues = mutableListOf()
|
||||
encodedQueryNamesAndValues!!.add(name.canonicalize(
|
||||
encodeSet = QUERY_COMPONENT_ENCODE_SET,
|
||||
plusIsSpace = true
|
||||
))
|
||||
encodedQueryNamesAndValues!!.add(value?.canonicalize(
|
||||
encodeSet = QUERY_COMPONENT_ENCODE_SET,
|
||||
plusIsSpace = true
|
||||
))
|
||||
}
|
||||
|
||||
/** Adds the pre-encoded query parameter to this URL's query string. */
|
||||
fun HttpUrl.Builder.commonAddEncodedQueryParameter(encodedName: String, encodedValue: String?) = apply {
|
||||
if (encodedQueryNamesAndValues == null) encodedQueryNamesAndValues = mutableListOf()
|
||||
encodedQueryNamesAndValues!!.add(encodedName.canonicalize(
|
||||
encodeSet = QUERY_COMPONENT_REENCODE_SET,
|
||||
alreadyEncoded = true,
|
||||
plusIsSpace = true
|
||||
))
|
||||
encodedQueryNamesAndValues!!.add(encodedValue?.canonicalize(
|
||||
encodeSet = QUERY_COMPONENT_REENCODE_SET,
|
||||
alreadyEncoded = true,
|
||||
plusIsSpace = true
|
||||
))
|
||||
}
|
||||
|
||||
fun HttpUrl.Builder.commonSetQueryParameter(name: String, value: String?) = apply {
|
||||
removeAllQueryParameters(name)
|
||||
addQueryParameter(name, value)
|
||||
}
|
||||
|
||||
fun HttpUrl.Builder.commonSetEncodedQueryParameter(encodedName: String, encodedValue: String?) = apply {
|
||||
removeAllEncodedQueryParameters(encodedName)
|
||||
addEncodedQueryParameter(encodedName, encodedValue)
|
||||
}
|
||||
|
||||
fun HttpUrl.Builder.commonRemoveAllQueryParameters(name: String) = apply {
|
||||
if (encodedQueryNamesAndValues == null) return this
|
||||
val nameToRemove = name.canonicalize(
|
||||
encodeSet = QUERY_COMPONENT_ENCODE_SET,
|
||||
plusIsSpace = true
|
||||
)
|
||||
commonRemoveAllCanonicalQueryParameters(nameToRemove)
|
||||
}
|
||||
|
||||
fun HttpUrl.Builder.commonRemoveAllEncodedQueryParameters(encodedName: String) = apply {
|
||||
if (encodedQueryNamesAndValues == null) return this
|
||||
commonRemoveAllCanonicalQueryParameters(encodedName.canonicalize(
|
||||
encodeSet = QUERY_COMPONENT_REENCODE_SET,
|
||||
alreadyEncoded = true,
|
||||
plusIsSpace = true
|
||||
))
|
||||
}
|
||||
|
||||
fun HttpUrl.Builder.commonRemoveAllCanonicalQueryParameters(canonicalName: String) {
|
||||
for (i in encodedQueryNamesAndValues!!.size - 2 downTo 0 step 2) {
|
||||
if (canonicalName == encodedQueryNamesAndValues!![i]) {
|
||||
encodedQueryNamesAndValues!!.removeAt(i + 1)
|
||||
encodedQueryNamesAndValues!!.removeAt(i)
|
||||
if (encodedQueryNamesAndValues!!.isEmpty()) {
|
||||
encodedQueryNamesAndValues = null
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun HttpUrl.Builder.commonFragment(fragment: String?) = apply {
|
||||
this.encodedFragment = fragment?.canonicalize(
|
||||
encodeSet = FRAGMENT_ENCODE_SET,
|
||||
unicodeAllowed = true
|
||||
)
|
||||
}
|
||||
|
||||
fun HttpUrl.Builder.commonEncodedFragment(encodedFragment: String?) = apply {
|
||||
this.encodedFragment = encodedFragment?.canonicalize(
|
||||
encodeSet = FRAGMENT_ENCODE_SET,
|
||||
alreadyEncoded = true,
|
||||
unicodeAllowed = true
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/** Adds a path segment. If the input is ".." or equivalent, this pops a path segment. */
|
||||
internal fun HttpUrl.Builder.push(
|
||||
input: String,
|
||||
pos: Int,
|
||||
limit: Int,
|
||||
addTrailingSlash: Boolean,
|
||||
alreadyEncoded: Boolean
|
||||
) {
|
||||
val segment = input.canonicalize(
|
||||
pos = pos,
|
||||
limit = limit,
|
||||
encodeSet = PATH_SEGMENT_ENCODE_SET,
|
||||
alreadyEncoded = alreadyEncoded
|
||||
)
|
||||
if (isDot(segment)) {
|
||||
return // Skip '.' path segments.
|
||||
}
|
||||
if (isDotDot(segment)) {
|
||||
pop()
|
||||
return
|
||||
}
|
||||
if (encodedPathSegments[encodedPathSegments.size - 1].isEmpty()) {
|
||||
encodedPathSegments[encodedPathSegments.size - 1] = segment
|
||||
} else {
|
||||
encodedPathSegments.add(segment)
|
||||
}
|
||||
if (addTrailingSlash) {
|
||||
encodedPathSegments.add("")
|
||||
}
|
||||
}
|
||||
|
||||
internal fun HttpUrl.Builder.isDot(input: String): Boolean {
|
||||
return input == "." || input.equals("%2e", ignoreCase = true)
|
||||
}
|
||||
|
||||
internal fun HttpUrl.Builder.isDotDot(input: String): Boolean {
|
||||
return input == ".." ||
|
||||
input.equals("%2e.", ignoreCase = true) ||
|
||||
input.equals(".%2e", ignoreCase = true) ||
|
||||
input.equals("%2e%2e", ignoreCase = true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a path segment. When this method returns the last segment is always "", which means
|
||||
* the encoded path will have a trailing '/'.
|
||||
*
|
||||
* Popping "/a/b/c/" yields "/a/b/". In this case the list of path segments goes from ["a",
|
||||
* "b", "c", ""] to ["a", "b", ""].
|
||||
*
|
||||
* Popping "/a/b/c" also yields "/a/b/". The list of path segments goes from ["a", "b", "c"]
|
||||
* to ["a", "b", ""].
|
||||
*/
|
||||
internal fun HttpUrl.Builder.pop() {
|
||||
val removed = encodedPathSegments.removeAt(encodedPathSegments.size - 1)
|
||||
|
||||
// Make sure the path ends with a '/' by either adding an empty string or clearing a segment.
|
||||
if (removed.isEmpty() && encodedPathSegments.isNotEmpty()) {
|
||||
encodedPathSegments[encodedPathSegments.size - 1] = ""
|
||||
} else {
|
||||
encodedPathSegments.add("")
|
||||
}
|
||||
}
|
||||
|
||||
internal fun HttpUrl.Builder.resolvePath(input: String, startPos: Int, limit: Int) {
|
||||
var pos = startPos
|
||||
// Read a delimiter.
|
||||
if (pos == limit) {
|
||||
// Empty path: keep the base path as-is.
|
||||
return
|
||||
}
|
||||
val c = input[pos]
|
||||
if (c == '/' || c == '\\') {
|
||||
// Absolute path: reset to the default "/".
|
||||
encodedPathSegments.clear()
|
||||
encodedPathSegments.add("")
|
||||
pos++
|
||||
} else {
|
||||
// Relative path: clear everything after the last '/'.
|
||||
encodedPathSegments[encodedPathSegments.size - 1] = ""
|
||||
}
|
||||
|
||||
// Read path segments.
|
||||
var i = pos
|
||||
while (i < limit) {
|
||||
val pathSegmentDelimiterOffset = input.delimiterOffset("/\\", i, limit)
|
||||
val segmentHasTrailingSlash = pathSegmentDelimiterOffset < limit
|
||||
push(input, i, pathSegmentDelimiterOffset, segmentHasTrailingSlash, true)
|
||||
i = pathSegmentDelimiterOffset
|
||||
if (segmentHasTrailingSlash) i++
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cuts this string up into alternating parameter names and values. This divides a query string
|
||||
* like `subject=math&easy&problem=5-2=3` into the list `["subject", "math", "easy", null,
|
||||
* "problem", "5-2=3"]`. Note that values may be null and may contain '=' characters.
|
||||
*/
|
||||
internal fun String.toQueryNamesAndValues(): MutableList<String?> {
|
||||
val result = mutableListOf<String?>()
|
||||
var pos = 0
|
||||
while (pos <= length) {
|
||||
var ampersandOffset = indexOf('&', pos)
|
||||
if (ampersandOffset == -1) ampersandOffset = length
|
||||
|
||||
val equalsOffset = indexOf('=', pos)
|
||||
if (equalsOffset == -1 || equalsOffset > ampersandOffset) {
|
||||
result.add(substring(pos, ampersandOffset))
|
||||
result.add(null) // No value for this name.
|
||||
} else {
|
||||
result.add(substring(pos, equalsOffset))
|
||||
result.add(substring(equalsOffset + 1, ampersandOffset))
|
||||
}
|
||||
pos = ampersandOffset + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun HttpUrl.Builder.commonBuild(): HttpUrl {
|
||||
@Suppress("UNCHECKED_CAST") // percentDecode returns either List<String?> or List<String>.
|
||||
return HttpUrl(
|
||||
scheme = scheme ?: throw IllegalStateException("scheme == null"),
|
||||
username = encodedUsername.percentDecode(),
|
||||
password = encodedPassword.percentDecode(),
|
||||
host = host ?: throw IllegalStateException("host == null"),
|
||||
port = effectivePort(),
|
||||
pathSegments = encodedPathSegments.map { it.percentDecode() },
|
||||
queryNamesAndValues = encodedQueryNamesAndValues?.map { it?.percentDecode(plusIsSpace = true) },
|
||||
fragment = encodedFragment?.percentDecode(),
|
||||
url = toString()
|
||||
)
|
||||
}
|
||||
|
||||
internal fun HttpUrl.Builder.effectivePort(): Int {
|
||||
return if (port != -1) port else HttpUrl.defaultPort(scheme!!)
|
||||
}
|
||||
internal fun HttpUrl.Builder.commonToString(): String {
|
||||
return buildString {
|
||||
if (scheme != null) {
|
||||
append(scheme)
|
||||
append("://")
|
||||
} else {
|
||||
append("//")
|
||||
}
|
||||
|
||||
if (encodedUsername.isNotEmpty() || encodedPassword.isNotEmpty()) {
|
||||
append(encodedUsername)
|
||||
if (encodedPassword.isNotEmpty()) {
|
||||
append(':')
|
||||
append(encodedPassword)
|
||||
}
|
||||
append('@')
|
||||
}
|
||||
|
||||
if (host != null) {
|
||||
if (':' in host!!) {
|
||||
// Host is an IPv6 address.
|
||||
append('[')
|
||||
append(host)
|
||||
append(']')
|
||||
} else {
|
||||
append(host)
|
||||
}
|
||||
}
|
||||
|
||||
if (port != -1 || scheme != null) {
|
||||
val effectivePort = effectivePort()
|
||||
if (scheme == null || effectivePort != HttpUrl.defaultPort(scheme!!)) {
|
||||
append(':')
|
||||
append(effectivePort)
|
||||
}
|
||||
}
|
||||
|
||||
encodedPathSegments.toPathString(this)
|
||||
|
||||
if (encodedQueryNamesAndValues != null) {
|
||||
append('?')
|
||||
encodedQueryNamesAndValues!!.toQueryString(this)
|
||||
}
|
||||
|
||||
if (encodedFragment != null) {
|
||||
append('#')
|
||||
append(encodedFragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns a path string for this list of path segments. */
|
||||
internal fun List<String>.toPathString(out: StringBuilder) {
|
||||
for (i in 0 until size) {
|
||||
out.append('/')
|
||||
out.append(this[i])
|
||||
}
|
||||
}
|
||||
|
||||
internal fun HttpUrl.Builder.commonParse(base: HttpUrl?, input: String): HttpUrl.Builder {
|
||||
var pos = input.indexOfFirstNonAsciiWhitespace()
|
||||
val limit = input.indexOfLastNonAsciiWhitespace(pos)
|
||||
|
||||
// Scheme.
|
||||
val schemeDelimiterOffset = schemeDelimiterOffset(input, pos, limit)
|
||||
if (schemeDelimiterOffset != -1) {
|
||||
when {
|
||||
input.startsWith("https:", ignoreCase = true, startIndex = pos) -> {
|
||||
this.scheme = "https"
|
||||
pos += "https:".length
|
||||
}
|
||||
input.startsWith("http:", ignoreCase = true, startIndex = pos) -> {
|
||||
this.scheme = "http"
|
||||
pos += "http:".length
|
||||
}
|
||||
else -> throw IllegalArgumentException("Expected URL scheme 'http' or 'https' but was '" +
|
||||
input.substring(0, schemeDelimiterOffset) + "'")
|
||||
}
|
||||
} else if (base != null) {
|
||||
this.scheme = base.scheme
|
||||
} else {
|
||||
val truncated = if (input.length > 6) input.take(6) + "..." else input
|
||||
throw IllegalArgumentException(
|
||||
"Expected URL scheme 'http' or 'https' but no scheme was found for $truncated")
|
||||
}
|
||||
|
||||
// Authority.
|
||||
var hasUsername = false
|
||||
var hasPassword = false
|
||||
val slashCount = input.slashCount(pos, limit)
|
||||
if (slashCount >= 2 || base == null || base.scheme != this.scheme) {
|
||||
// Read an authority if either:
|
||||
// * The input starts with 2 or more slashes. These follow the scheme if it exists.
|
||||
// * The input scheme exists and is different from the base URL's scheme.
|
||||
//
|
||||
// The structure of an authority is:
|
||||
// username:password@host:port
|
||||
//
|
||||
// Username, password and port are optional.
|
||||
// [username[:password]@]host[:port]
|
||||
pos += slashCount
|
||||
authority@ while (true) {
|
||||
val componentDelimiterOffset = input.delimiterOffset("@/\\?#", pos, limit)
|
||||
val c = if (componentDelimiterOffset != limit) {
|
||||
input[componentDelimiterOffset].code
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
when (c) {
|
||||
'@'.code -> {
|
||||
// User info precedes.
|
||||
if (!hasPassword) {
|
||||
val passwordColonOffset = input.delimiterOffset(':', pos, componentDelimiterOffset)
|
||||
val canonicalUsername = input.canonicalize(
|
||||
pos = pos,
|
||||
limit = passwordColonOffset,
|
||||
encodeSet = USERNAME_ENCODE_SET,
|
||||
alreadyEncoded = true
|
||||
)
|
||||
this.encodedUsername = if (hasUsername) {
|
||||
this.encodedUsername + "%40" + canonicalUsername
|
||||
} else {
|
||||
canonicalUsername
|
||||
}
|
||||
if (passwordColonOffset != componentDelimiterOffset) {
|
||||
hasPassword = true
|
||||
this.encodedPassword = input.canonicalize(
|
||||
pos = passwordColonOffset + 1,
|
||||
limit = componentDelimiterOffset,
|
||||
encodeSet = PASSWORD_ENCODE_SET,
|
||||
alreadyEncoded = true
|
||||
)
|
||||
}
|
||||
hasUsername = true
|
||||
} else {
|
||||
this.encodedPassword = this.encodedPassword + "%40" + input.canonicalize(
|
||||
pos = pos,
|
||||
limit = componentDelimiterOffset,
|
||||
encodeSet = PASSWORD_ENCODE_SET,
|
||||
alreadyEncoded = true
|
||||
)
|
||||
}
|
||||
pos = componentDelimiterOffset + 1
|
||||
}
|
||||
|
||||
-1, '/'.code, '\\'.code, '?'.code, '#'.code -> {
|
||||
// Host info precedes.
|
||||
val portColonOffset = portColonOffset(input, pos, componentDelimiterOffset)
|
||||
if (portColonOffset + 1 < componentDelimiterOffset) {
|
||||
host = input.percentDecode(pos = pos, limit = portColonOffset).toCanonicalHost()
|
||||
port = parsePort(input, portColonOffset + 1, componentDelimiterOffset)
|
||||
require(port != -1) {
|
||||
"Invalid URL port: \"${input.substring(portColonOffset + 1,
|
||||
componentDelimiterOffset)}\""
|
||||
}
|
||||
} else {
|
||||
host = input.percentDecode(pos = pos, limit = portColonOffset).toCanonicalHost()
|
||||
port = HttpUrl.defaultPort(scheme!!)
|
||||
}
|
||||
require(host != null) {
|
||||
"$INVALID_HOST: \"${input.substring(pos, portColonOffset)}\""
|
||||
}
|
||||
pos = componentDelimiterOffset
|
||||
break@authority
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// This is a relative link. Copy over all authority components. Also maybe the path & query.
|
||||
this.encodedUsername = base.encodedUsername
|
||||
this.encodedPassword = base.encodedPassword
|
||||
this.host = base.host
|
||||
this.port = base.port
|
||||
this.encodedPathSegments.clear()
|
||||
this.encodedPathSegments.addAll(base.encodedPathSegments)
|
||||
if (pos == limit || input[pos] == '#') {
|
||||
encodedQuery(base.encodedQuery)
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve the relative path.
|
||||
val pathDelimiterOffset = input.delimiterOffset("?#", pos, limit)
|
||||
resolvePath(input, pos, pathDelimiterOffset)
|
||||
pos = pathDelimiterOffset
|
||||
|
||||
// Query.
|
||||
if (pos < limit && input[pos] == '?') {
|
||||
val queryDelimiterOffset = input.delimiterOffset('#', pos, limit)
|
||||
this.encodedQueryNamesAndValues = input.canonicalize(
|
||||
pos = pos + 1,
|
||||
limit = queryDelimiterOffset,
|
||||
encodeSet = QUERY_ENCODE_SET,
|
||||
alreadyEncoded = true,
|
||||
plusIsSpace = true
|
||||
).toQueryNamesAndValues()
|
||||
pos = queryDelimiterOffset
|
||||
}
|
||||
|
||||
// Fragment.
|
||||
if (pos < limit && input[pos] == '#') {
|
||||
this.encodedFragment = input.canonicalize(
|
||||
pos = pos + 1,
|
||||
limit = limit,
|
||||
encodeSet = FRAGMENT_ENCODE_SET,
|
||||
alreadyEncoded = true,
|
||||
unicodeAllowed = true
|
||||
)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
internal const val INVALID_HOST = "Invalid URL host"
|
||||
|
||||
/**
|
||||
* Returns the index of the ':' in `input` that is after scheme characters. Returns -1 if
|
||||
* `input` does not have a scheme that starts at `pos`.
|
||||
*/
|
||||
internal fun schemeDelimiterOffset(input: String, pos: Int, limit: Int): Int {
|
||||
if (limit - pos < 2) return -1
|
||||
|
||||
val c0 = input[pos]
|
||||
if ((c0 < 'a' || c0 > 'z') && (c0 < 'A' || c0 > 'Z')) return -1 // Not a scheme start char.
|
||||
|
||||
characters@ for (i in pos + 1 until limit) {
|
||||
return when (input[i]) {
|
||||
// Scheme character. Keep going.
|
||||
in 'a'..'z', in 'A'..'Z', in '0'..'9', '+', '-', '.' -> continue@characters
|
||||
|
||||
// Scheme prefix!
|
||||
':' -> i
|
||||
|
||||
// Non-scheme character before the first ':'.
|
||||
else -> -1
|
||||
}
|
||||
}
|
||||
|
||||
return -1 // No ':'; doesn't start with a scheme.
|
||||
}
|
||||
|
||||
/** Returns the number of '/' and '\' slashes in this, starting at `pos`. */
|
||||
internal fun String.slashCount(pos: Int, limit: Int): Int {
|
||||
var slashCount = 0
|
||||
for (i in pos until limit) {
|
||||
val c = this[i]
|
||||
if (c == '\\' || c == '/') {
|
||||
slashCount++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return slashCount
|
||||
}
|
||||
|
||||
/** Finds the first ':' in `input`, skipping characters between square braces "[...]". */
|
||||
internal fun portColonOffset(input: String, pos: Int, limit: Int): Int {
|
||||
var i = pos
|
||||
while (i < limit) {
|
||||
when (input[i]) {
|
||||
'[' -> {
|
||||
while (++i < limit) {
|
||||
if (input[i] == ']') break
|
||||
}
|
||||
}
|
||||
':' -> return i
|
||||
}
|
||||
i++
|
||||
}
|
||||
return limit // No colon.
|
||||
}
|
||||
|
||||
internal fun parsePort(input: String, pos: Int, limit: Int): Int {
|
||||
return try {
|
||||
// Canonicalize the port string to skip '\n' etc.
|
||||
val portString = input.canonicalize(pos = pos, limit = limit, encodeSet = "")
|
||||
val i = portString.toInt()
|
||||
if (i in 1..65535) i else -1
|
||||
} catch (_: NumberFormatException) {
|
||||
-1 // Invalid port.
|
||||
}
|
||||
}
|
||||
|
||||
internal fun String.isPercentEncoded(pos: Int, limit: Int): Boolean {
|
||||
return pos + 2 < limit &&
|
||||
this[pos] == '%' &&
|
||||
this[pos + 1].parseHexDigit() != -1 &&
|
||||
this[pos + 2].parseHexDigit() != -1
|
||||
}
|
||||
|
||||
internal fun String.commonToHttpUrl(): HttpUrl = HttpUrl.Builder().parse(null, this).build()
|
||||
|
||||
internal fun String.commonToHttpUrlOrNull(): HttpUrl? {
|
||||
return try {
|
||||
commonToHttpUrl()
|
||||
} catch (_: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
@@ -34,9 +34,6 @@ import okio.Options
|
||||
import okio.Path
|
||||
import okio.use
|
||||
|
||||
// Temporary until we have a HttpUrl in common
|
||||
expect class HttpUrlRepresentation
|
||||
|
||||
// TODO: migrate callers to [Regex.matchAt] when that API is not experimental.
|
||||
internal fun Regex.matchAtPolyfill(input: CharSequence, index: Int): MatchResult? {
|
||||
val candidate = find(input, index) ?: return null
|
||||
@@ -394,3 +391,9 @@ internal fun <T> interleave(a: Iterable<T>, b: Iterable<T>): List<T> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO check read only options for creating lists
|
||||
public fun <T> List<T>.readOnly() = this.toList()
|
||||
|
||||
// TODO check read only options for creating lists
|
||||
public fun <T> Set<T>.readOnly() = this.toSet()
|
||||
|
1334
okhttp/src/commonTest/kotlin/okhttp3/HttpUrlCommonTest.kt
Normal file
1334
okhttp/src/commonTest/kotlin/okhttp3/HttpUrlCommonTest.kt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,7 @@ import kotlin.test.assertEquals
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
|
||||
class MediaTypeJsTest {
|
||||
open fun MediaType.charsetName(): String? = parameter("charset")
|
||||
fun MediaType.charsetName(): String? = parameter("charset")
|
||||
|
||||
@Test
|
||||
fun testIllegalCharsetName() {
|
||||
|
@@ -17,10 +17,10 @@ package okhttp3
|
||||
|
||||
import java.io.IOException
|
||||
import java.nio.charset.Charset
|
||||
import okhttp3.HttpUrl.Companion.FORM_ENCODE_SET
|
||||
import okhttp3.HttpUrl.Companion.canonicalize
|
||||
import okhttp3.HttpUrl.Companion.percentDecode
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.internal.CommonHttpUrl.FORM_ENCODE_SET
|
||||
import okhttp3.internal.CommonHttpUrl.percentDecode
|
||||
import okhttp3.internal.JvmHttpUrl.canonicalizeWithCharset
|
||||
import okhttp3.internal.toImmutableList
|
||||
import okio.Buffer
|
||||
import okio.BufferedSink
|
||||
@@ -90,12 +90,12 @@ class FormBody internal constructor(
|
||||
private val values = mutableListOf<String>()
|
||||
|
||||
fun add(name: String, value: String) = apply {
|
||||
names += name.canonicalize(
|
||||
names += name.canonicalizeWithCharset(
|
||||
encodeSet = FORM_ENCODE_SET,
|
||||
plusIsSpace = false, // plus is encoded as `%2B`, space is encoded as plus.
|
||||
charset = charset
|
||||
)
|
||||
values += value.canonicalize(
|
||||
values += value.canonicalizeWithCharset(
|
||||
encodeSet = FORM_ENCODE_SET,
|
||||
plusIsSpace = false, // plus is encoded as `%2B`, space is encoded as plus.
|
||||
charset = charset
|
||||
@@ -103,13 +103,13 @@ class FormBody internal constructor(
|
||||
}
|
||||
|
||||
fun addEncoded(name: String, value: String) = apply {
|
||||
names += name.canonicalize(
|
||||
names += name.canonicalizeWithCharset(
|
||||
encodeSet = FORM_ENCODE_SET,
|
||||
alreadyEncoded = true,
|
||||
plusIsSpace = true,
|
||||
charset = charset
|
||||
)
|
||||
values += value.canonicalize(
|
||||
values += value.canonicalizeWithCharset(
|
||||
encodeSet = FORM_ENCODE_SET,
|
||||
alreadyEncoded = true,
|
||||
plusIsSpace = true,
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -201,7 +201,7 @@ actual class Request internal actual constructor(builder: Builder) {
|
||||
this.headers = request.headers.newBuilder()
|
||||
}
|
||||
|
||||
open fun url(url: HttpUrl): Builder = apply {
|
||||
actual open fun url(url: HttpUrl): Builder = apply {
|
||||
this.url = url
|
||||
}
|
||||
|
||||
|
@@ -27,7 +27,7 @@ import java.util.Locale
|
||||
* `null` will be returned if the host cannot be ToASCII encoded or if the result contains
|
||||
* unsupported ASCII characters.
|
||||
*/
|
||||
fun String.toCanonicalHost(): String? {
|
||||
actual fun String.toCanonicalHost(): String? {
|
||||
val host: String = this
|
||||
|
||||
// If the input contains a :, it’s an IPv6 address.
|
||||
|
197
okhttp/src/jvmMain/kotlin/okhttp3/internal/-HttpUrlJvm.kt
Normal file
197
okhttp/src/jvmMain/kotlin/okhttp3/internal/-HttpUrlJvm.kt
Normal file
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package okhttp3.internal
|
||||
|
||||
import java.nio.charset.Charset
|
||||
import okhttp3.internal.CommonHttpUrl.FORM_ENCODE_SET
|
||||
import okhttp3.internal.CommonHttpUrl.HEX_DIGITS
|
||||
import okhttp3.internal.CommonHttpUrl.isPercentEncoded
|
||||
import okhttp3.internal.JvmHttpUrl.canonicalizeWithCharset
|
||||
import okio.Buffer
|
||||
|
||||
internal object JvmHttpUrl {
|
||||
|
||||
internal fun Buffer.writeCanonicalized(
|
||||
input: String,
|
||||
pos: Int,
|
||||
limit: Int,
|
||||
encodeSet: String,
|
||||
alreadyEncoded: Boolean,
|
||||
strict: Boolean,
|
||||
plusIsSpace: Boolean,
|
||||
unicodeAllowed: Boolean,
|
||||
charset: Charset?
|
||||
) {
|
||||
var encodedCharBuffer: Buffer? = null // Lazily allocated.
|
||||
var codePoint: Int
|
||||
var i = pos
|
||||
while (i < limit) {
|
||||
codePoint = input.codePointAt(i)
|
||||
if (alreadyEncoded && (codePoint == '\t'.code || codePoint == '\n'.code ||
|
||||
codePoint == '\u000c'.code || codePoint == '\r'.code)) {
|
||||
// Skip this character.
|
||||
} else if (codePoint == ' '.code && encodeSet === FORM_ENCODE_SET) {
|
||||
// Encode ' ' as '+'.
|
||||
writeUtf8("+")
|
||||
} else if (codePoint == '+'.code && plusIsSpace) {
|
||||
// Encode '+' as '%2B' since we permit ' ' to be encoded as either '+' or '%20'.
|
||||
writeUtf8(if (alreadyEncoded) "+" else "%2B")
|
||||
} else if (codePoint < 0x20 ||
|
||||
codePoint == 0x7f ||
|
||||
codePoint >= 0x80 && !unicodeAllowed ||
|
||||
codePoint.toChar() in encodeSet ||
|
||||
codePoint == '%'.code &&
|
||||
(!alreadyEncoded || strict && !input.isPercentEncoded(i, limit))) {
|
||||
// Percent encode this character.
|
||||
if (encodedCharBuffer == null) {
|
||||
encodedCharBuffer = Buffer()
|
||||
}
|
||||
|
||||
if (charset == null || charset == Charsets.UTF_8) {
|
||||
encodedCharBuffer.writeUtf8CodePoint(codePoint)
|
||||
} else {
|
||||
encodedCharBuffer.writeString(input, i, i + Character.charCount(codePoint), charset)
|
||||
}
|
||||
|
||||
while (!encodedCharBuffer.exhausted()) {
|
||||
val b = encodedCharBuffer.readByte().toInt() and 0xff
|
||||
writeByte('%'.code)
|
||||
writeByte(HEX_DIGITS[b shr 4 and 0xf].code)
|
||||
writeByte(HEX_DIGITS[b and 0xf].code)
|
||||
}
|
||||
} else {
|
||||
// This character doesn't need encoding. Just copy it over.
|
||||
writeUtf8CodePoint(codePoint)
|
||||
}
|
||||
i += Character.charCount(codePoint)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a substring of `input` on the range `[pos..limit)` with the following
|
||||
* transformations:
|
||||
*
|
||||
* * Tabs, newlines, form feeds and carriage returns are skipped.
|
||||
*
|
||||
* * In queries, ' ' is encoded to '+' and '+' is encoded to "%2B".
|
||||
*
|
||||
* * Characters in `encodeSet` are percent-encoded.
|
||||
*
|
||||
* * Control characters and non-ASCII characters are percent-encoded.
|
||||
*
|
||||
* * All other characters are copied without transformation.
|
||||
*
|
||||
* @param alreadyEncoded true to leave '%' as-is; false to convert it to '%25'.
|
||||
* @param strict true to encode '%' if it is not the prefix of a valid percent encoding.
|
||||
* @param plusIsSpace true to encode '+' as "%2B" if it is not already encoded.
|
||||
* @param unicodeAllowed true to leave non-ASCII codepoint unencoded.
|
||||
* @param charset which charset to use, null equals UTF-8.
|
||||
*/
|
||||
internal fun String.canonicalizeWithCharset(
|
||||
pos: Int = 0,
|
||||
limit: Int = length,
|
||||
encodeSet: String,
|
||||
alreadyEncoded: Boolean = false,
|
||||
strict: Boolean = false,
|
||||
plusIsSpace: Boolean = false,
|
||||
unicodeAllowed: Boolean = false,
|
||||
charset: Charset? = null
|
||||
): String {
|
||||
var codePoint: Int
|
||||
var i = pos
|
||||
while (i < limit) {
|
||||
codePoint = codePointAt(i)
|
||||
if (codePoint < 0x20 ||
|
||||
codePoint == 0x7f ||
|
||||
codePoint >= 0x80 && !unicodeAllowed ||
|
||||
codePoint.toChar() in encodeSet ||
|
||||
codePoint == '%'.code &&
|
||||
(!alreadyEncoded || strict && !isPercentEncoded(i, limit)) ||
|
||||
codePoint == '+'.code && plusIsSpace) {
|
||||
// Slow path: the character at i requires encoding!
|
||||
val out = Buffer()
|
||||
out.writeUtf8(this, pos, i)
|
||||
out.writeCanonicalized(
|
||||
input = this,
|
||||
pos = i,
|
||||
limit = limit,
|
||||
encodeSet = encodeSet,
|
||||
alreadyEncoded = alreadyEncoded,
|
||||
strict = strict,
|
||||
plusIsSpace = plusIsSpace,
|
||||
unicodeAllowed = unicodeAllowed,
|
||||
charset = charset
|
||||
)
|
||||
return out.readUtf8()
|
||||
}
|
||||
i += Character.charCount(codePoint)
|
||||
}
|
||||
|
||||
// Fast path: no characters in [pos..limit) required encoding.
|
||||
return substring(pos, limit)
|
||||
}
|
||||
}
|
||||
|
||||
internal actual object HttpUrlCommon {
|
||||
internal actual fun Buffer.writePercentDecoded(
|
||||
encoded: String,
|
||||
pos: Int,
|
||||
limit: Int,
|
||||
plusIsSpace: Boolean
|
||||
) {
|
||||
var codePoint: Int
|
||||
var i = pos
|
||||
while (i < limit) {
|
||||
codePoint = encoded.codePointAt(i)
|
||||
if (codePoint == '%'.code && i + 2 < limit) {
|
||||
val d1 = encoded[i + 1].parseHexDigit()
|
||||
val d2 = encoded[i + 2].parseHexDigit()
|
||||
if (d1 != -1 && d2 != -1) {
|
||||
writeByte((d1 shl 4) + d2)
|
||||
i += 2
|
||||
i += Character.charCount(codePoint)
|
||||
continue
|
||||
}
|
||||
} else if (codePoint == '+'.code && plusIsSpace) {
|
||||
writeByte(' '.code)
|
||||
i++
|
||||
continue
|
||||
}
|
||||
writeUtf8CodePoint(codePoint)
|
||||
i += Character.charCount(codePoint)
|
||||
}
|
||||
}
|
||||
internal actual fun String.canonicalize(
|
||||
pos: Int,
|
||||
limit: Int,
|
||||
encodeSet: String,
|
||||
alreadyEncoded: Boolean,
|
||||
strict: Boolean,
|
||||
plusIsSpace: Boolean,
|
||||
unicodeAllowed: Boolean,
|
||||
): String {
|
||||
return canonicalizeWithCharset(
|
||||
pos = pos,
|
||||
limit = limit,
|
||||
encodeSet = encodeSet,
|
||||
alreadyEncoded = alreadyEncoded,
|
||||
strict = strict,
|
||||
plusIsSpace = plusIsSpace,
|
||||
unicodeAllowed = unicodeAllowed
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@@ -39,6 +39,7 @@ import okhttp3.OkHttpClient
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody
|
||||
import okhttp3.internal.CommonHttpUrl.commonDefaultPort
|
||||
import okhttp3.internal.http2.Header
|
||||
import okio.Buffer
|
||||
import okio.BufferedSource
|
||||
@@ -51,8 +52,6 @@ val EMPTY_REQUEST: RequestBody = commonEmptyRequestBody
|
||||
@JvmField
|
||||
val EMPTY_RESPONSE: ResponseBody = commonEmptyResponse
|
||||
|
||||
actual typealias HttpUrlRepresentation = HttpUrl
|
||||
|
||||
/** GMT and UTC are equivalent for our purposes. */
|
||||
@JvmField
|
||||
internal val UTC: TimeZone = TimeZone.getTimeZone("GMT")!!
|
||||
@@ -72,7 +71,7 @@ internal fun HttpUrl.toHostHeader(includeDefaultPort: Boolean = false): String {
|
||||
} else {
|
||||
host
|
||||
}
|
||||
return if (includeDefaultPort || port != HttpUrl.defaultPort(scheme)) {
|
||||
return if (includeDefaultPort || port != commonDefaultPort(scheme)) {
|
||||
"$host:$port"
|
||||
} else {
|
||||
host
|
||||
|
@@ -125,58 +125,6 @@ open class HttpUrlTest {
|
||||
assertThat(parse("http://h/\u3000").encodedPath).isEqualTo("/%E3%80%80")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun scheme() {
|
||||
assertThat(parse("http://host/")).isEqualTo(parse("http://host/"))
|
||||
assertThat(parse("Http://host/")).isEqualTo(parse("http://host/"))
|
||||
assertThat(parse("http://host/")).isEqualTo(parse("http://host/"))
|
||||
assertThat(parse("HTTP://host/")).isEqualTo(parse("http://host/"))
|
||||
assertThat(parse("https://host/")).isEqualTo(parse("https://host/"))
|
||||
assertThat(parse("HTTPS://host/")).isEqualTo(parse("https://host/"))
|
||||
assertInvalid(
|
||||
"image640://480.png",
|
||||
"Expected URL scheme 'http' or 'https' but was 'image640'"
|
||||
)
|
||||
assertInvalid("httpp://host/", "Expected URL scheme 'http' or 'https' but was 'httpp'")
|
||||
assertInvalid(
|
||||
"0ttp://host/",
|
||||
"Expected URL scheme 'http' or 'https' but no scheme was found for 0ttp:/..."
|
||||
)
|
||||
assertInvalid("ht+tp://host/", "Expected URL scheme 'http' or 'https' but was 'ht+tp'")
|
||||
assertInvalid("ht.tp://host/", "Expected URL scheme 'http' or 'https' but was 'ht.tp'")
|
||||
assertInvalid("ht-tp://host/", "Expected URL scheme 'http' or 'https' but was 'ht-tp'")
|
||||
assertInvalid("ht1tp://host/", "Expected URL scheme 'http' or 'https' but was 'ht1tp'")
|
||||
assertInvalid("httpss://host/", "Expected URL scheme 'http' or 'https' but was 'httpss'")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseNoScheme() {
|
||||
assertInvalid(
|
||||
"//host",
|
||||
"Expected URL scheme 'http' or 'https' but no scheme was found for //host"
|
||||
)
|
||||
assertInvalid(
|
||||
"://host",
|
||||
"Expected URL scheme 'http' or 'https' but no scheme was found for ://hos..."
|
||||
)
|
||||
assertInvalid(
|
||||
"/path",
|
||||
"Expected URL scheme 'http' or 'https' but no scheme was found for /path"
|
||||
)
|
||||
assertInvalid(
|
||||
"path",
|
||||
"Expected URL scheme 'http' or 'https' but no scheme was found for path"
|
||||
)
|
||||
assertInvalid(
|
||||
"?query",
|
||||
"Expected URL scheme 'http' or 'https' but no scheme was found for ?query"
|
||||
)
|
||||
assertInvalid(
|
||||
"#fragment",
|
||||
"Expected URL scheme 'http' or 'https' but no scheme was found for #fragm..."
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun newBuilderResolve() {
|
||||
// Non-exhaustive tests because implementation is the same as resolve.
|
||||
@@ -914,18 +862,6 @@ open class HttpUrlTest {
|
||||
.isEqualTo("http://[::1]/")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun hostIpv4CanonicalForm() {
|
||||
assertThat(parse("http://255.255.255.255/").host).isEqualTo("255.255.255.255")
|
||||
assertThat(parse("http://1.2.3.4/").host).isEqualTo("1.2.3.4")
|
||||
assertThat(parse("http://0.0.0.0/").host).isEqualTo("0.0.0.0")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun hostWithTrailingDot() {
|
||||
assertThat(parse("http://host./").host).isEqualTo("host.")
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip unexpected characters when converting to URI (which is more strict).
|
||||
* https://github.com/square/okhttp/issues/5667
|
||||
@@ -945,19 +881,6 @@ open class HttpUrlTest {
|
||||
assertThat(httpUrl.toUri().toString()).isEqualTo("http://\$tracker/")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun port() {
|
||||
assertThat(parse("http://host:80/")).isEqualTo(parse("http://host/"))
|
||||
assertThat(parse("http://host:99/")).isEqualTo(parse("http://host:99/"))
|
||||
assertThat(parse("http://host:/")).isEqualTo(parse("http://host/"))
|
||||
assertThat(parse("http://host:65535/").port).isEqualTo(65535)
|
||||
assertInvalid("http://host:0/", "Invalid URL port: \"0\"")
|
||||
assertInvalid("http://host:65536/", "Invalid URL port: \"65536\"")
|
||||
assertInvalid("http://host:-1/", "Invalid URL port: \"-1\"")
|
||||
assertInvalid("http://host:a/", "Invalid URL port: \"a\"")
|
||||
assertInvalid("http://host:%39%39/", "Invalid URL port: \"%39%39\"")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pathCharacters() {
|
||||
UrlComponentEncodingTester.newInstance()
|
||||
@@ -1224,44 +1147,6 @@ open class HttpUrlTest {
|
||||
.isEqualTo("//host.com:8080/path")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun minimalUrlComposition() {
|
||||
val url = HttpUrl.Builder().scheme("http").host("host").build()
|
||||
assertThat(url.toString()).isEqualTo("http://host/")
|
||||
assertThat(url.scheme).isEqualTo("http")
|
||||
assertThat(url.username).isEqualTo("")
|
||||
assertThat(url.password).isEqualTo("")
|
||||
assertThat(url.host).isEqualTo("host")
|
||||
assertThat(url.port).isEqualTo(80)
|
||||
assertThat(url.encodedPath).isEqualTo("/")
|
||||
assertThat(url.query).isNull()
|
||||
assertThat(url.fragment).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fullUrlComposition() {
|
||||
val url = HttpUrl.Builder()
|
||||
.scheme("http")
|
||||
.username("username")
|
||||
.password("password")
|
||||
.host("host")
|
||||
.port(8080)
|
||||
.addPathSegment("path")
|
||||
.query("query")
|
||||
.fragment("fragment")
|
||||
.build()
|
||||
assertThat(url.toString())
|
||||
.isEqualTo("http://username:password@host:8080/path?query#fragment")
|
||||
assertThat(url.scheme).isEqualTo("http")
|
||||
assertThat(url.username).isEqualTo("username")
|
||||
assertThat(url.password).isEqualTo("password")
|
||||
assertThat(url.host).isEqualTo("host")
|
||||
assertThat(url.port).isEqualTo(8080)
|
||||
assertThat(url.encodedPath).isEqualTo("/path")
|
||||
assertThat(url.query).isEqualTo("query")
|
||||
assertThat(url.fragment).isEqualTo("fragment")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun changingSchemeChangesDefaultPort() {
|
||||
assertThat(
|
||||
@@ -1422,12 +1307,6 @@ open class HttpUrlTest {
|
||||
.isEqualTo("/a/b/c/")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pathSize() {
|
||||
assertThat(parse("http://host/").pathSize).isEqualTo(1)
|
||||
assertThat(parse("http://host/a/b/c").pathSize).isEqualTo(3)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addPathSegments() {
|
||||
val base = parse("http://host/a/b/c")
|
||||
|
479
okhttp/src/nonJvmMain/kotlin/okhttp3/HttpUrl.kt
Normal file
479
okhttp/src/nonJvmMain/kotlin/okhttp3/HttpUrl.kt
Normal file
@@ -0,0 +1,479 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package okhttp3
|
||||
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.internal.CommonHttpUrl
|
||||
import okhttp3.internal.CommonHttpUrl.commonAddEncodedPathSegment
|
||||
import okhttp3.internal.CommonHttpUrl.commonAddEncodedPathSegments
|
||||
import okhttp3.internal.CommonHttpUrl.commonAddEncodedQueryParameter
|
||||
import okhttp3.internal.CommonHttpUrl.commonAddPathSegment
|
||||
import okhttp3.internal.CommonHttpUrl.commonAddPathSegments
|
||||
import okhttp3.internal.CommonHttpUrl.commonAddQueryParameter
|
||||
import okhttp3.internal.CommonHttpUrl.commonBuild
|
||||
import okhttp3.internal.CommonHttpUrl.commonEncodedFragment
|
||||
import okhttp3.internal.CommonHttpUrl.commonEncodedPassword
|
||||
import okhttp3.internal.CommonHttpUrl.commonEncodedPath
|
||||
import okhttp3.internal.CommonHttpUrl.commonEncodedPathSegments
|
||||
import okhttp3.internal.CommonHttpUrl.commonEncodedQuery
|
||||
import okhttp3.internal.CommonHttpUrl.commonEncodedUsername
|
||||
import okhttp3.internal.CommonHttpUrl.commonEquals
|
||||
import okhttp3.internal.CommonHttpUrl.commonFragment
|
||||
import okhttp3.internal.CommonHttpUrl.commonHashCode
|
||||
import okhttp3.internal.CommonHttpUrl.commonHost
|
||||
import okhttp3.internal.CommonHttpUrl.commonIsHttps
|
||||
import okhttp3.internal.CommonHttpUrl.commonNewBuilder
|
||||
import okhttp3.internal.CommonHttpUrl.commonParse
|
||||
import okhttp3.internal.CommonHttpUrl.commonPassword
|
||||
import okhttp3.internal.CommonHttpUrl.commonPathSize
|
||||
import okhttp3.internal.CommonHttpUrl.commonPort
|
||||
import okhttp3.internal.CommonHttpUrl.commonQuery
|
||||
import okhttp3.internal.CommonHttpUrl.commonQueryParameter
|
||||
import okhttp3.internal.CommonHttpUrl.commonQueryParameterName
|
||||
import okhttp3.internal.CommonHttpUrl.commonQueryParameterNames
|
||||
import okhttp3.internal.CommonHttpUrl.commonQueryParameterValue
|
||||
import okhttp3.internal.CommonHttpUrl.commonQueryParameterValues
|
||||
import okhttp3.internal.CommonHttpUrl.commonQuerySize
|
||||
import okhttp3.internal.CommonHttpUrl.commonRedact
|
||||
import okhttp3.internal.CommonHttpUrl.commonRemoveAllEncodedQueryParameters
|
||||
import okhttp3.internal.CommonHttpUrl.commonRemoveAllQueryParameters
|
||||
import okhttp3.internal.CommonHttpUrl.commonRemovePathSegment
|
||||
import okhttp3.internal.CommonHttpUrl.commonResolve
|
||||
import okhttp3.internal.CommonHttpUrl.commonScheme
|
||||
import okhttp3.internal.CommonHttpUrl.commonSetEncodedPathSegment
|
||||
import okhttp3.internal.CommonHttpUrl.commonSetEncodedQueryParameter
|
||||
import okhttp3.internal.CommonHttpUrl.commonSetPathSegment
|
||||
import okhttp3.internal.CommonHttpUrl.commonSetQueryParameter
|
||||
import okhttp3.internal.CommonHttpUrl.commonToHttpUrl
|
||||
import okhttp3.internal.CommonHttpUrl.commonToHttpUrlOrNull
|
||||
import okhttp3.internal.CommonHttpUrl.commonToString
|
||||
import okhttp3.internal.CommonHttpUrl.commonUsername
|
||||
|
||||
/**
|
||||
* A uniform resource locator (URL) with a scheme of either `http` or `https`. Use this class to
|
||||
* compose and decompose Internet addresses. For example, this code will compose and print a URL for
|
||||
* Google search:
|
||||
*
|
||||
* ```java
|
||||
* HttpUrl url = new HttpUrl.Builder()
|
||||
* .scheme("https")
|
||||
* .host("www.google.com")
|
||||
* .addPathSegment("search")
|
||||
* .addQueryParameter("q", "polar bears")
|
||||
* .build();
|
||||
* System.out.println(url);
|
||||
* ```
|
||||
*
|
||||
* which prints:
|
||||
*
|
||||
* ```
|
||||
* https://www.google.com/search?q=polar%20bears
|
||||
* ```
|
||||
*
|
||||
* As another example, this code prints the human-readable query parameters of a Twitter search:
|
||||
*
|
||||
* ```java
|
||||
* HttpUrl url = HttpUrl.parse("https://twitter.com/search?q=cute%20%23puppies&f=images");
|
||||
* for (int i = 0, size = url.querySize(); i < size; i++) {
|
||||
* System.out.println(url.queryParameterName(i) + ": " + url.queryParameterValue(i));
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* which prints:
|
||||
*
|
||||
* ```
|
||||
* q: cute #puppies
|
||||
* f: images
|
||||
* ```
|
||||
*
|
||||
* In addition to composing URLs from their component parts and decomposing URLs into their
|
||||
* component parts, this class implements relative URL resolution: what address you'd reach by
|
||||
* clicking a relative link on a specified page. For example:
|
||||
*
|
||||
* ```java
|
||||
* HttpUrl base = HttpUrl.parse("https://www.youtube.com/user/WatchTheDaily/videos");
|
||||
* HttpUrl link = base.resolve("../../watch?v=cbP2N1BQdYc");
|
||||
* System.out.println(link);
|
||||
* ```
|
||||
*
|
||||
* which prints:
|
||||
*
|
||||
* ```
|
||||
* https://www.youtube.com/watch?v=cbP2N1BQdYc
|
||||
* ```
|
||||
*
|
||||
* ## What's in a URL?
|
||||
*
|
||||
* A URL has several components.
|
||||
*
|
||||
* ### Scheme
|
||||
*
|
||||
* Sometimes referred to as *protocol*, A URL's scheme describes what mechanism should be used to
|
||||
* retrieve the resource. Although URLs have many schemes (`mailto`, `file`, `ftp`), this class only
|
||||
* supports `http` and `https`. Use [java.net.URI][URI] for URLs with arbitrary schemes.
|
||||
*
|
||||
* ### Username and Password
|
||||
*
|
||||
* Username and password are either present, or the empty string `""` if absent. This class offers
|
||||
* no mechanism to differentiate empty from absent. Neither of these components are popular in
|
||||
* practice. Typically HTTP applications use other mechanisms for user identification and
|
||||
* authentication.
|
||||
*
|
||||
* ### Host
|
||||
*
|
||||
* The host identifies the webserver that serves the URL's resource. It is either a hostname like
|
||||
* `square.com` or `localhost`, an IPv4 address like `192.168.0.1`, or an IPv6 address like `::1`.
|
||||
*
|
||||
* Usually a webserver is reachable with multiple identifiers: its IP addresses, registered
|
||||
* domain names, and even `localhost` when connecting from the server itself. Each of a web server's
|
||||
* names is a distinct URL and they are not interchangeable. For example, even if
|
||||
* `http://square.github.io/dagger` and `http://google.github.io/dagger` are served by the same IP
|
||||
* address, the two URLs identify different resources.
|
||||
*
|
||||
* ### Port
|
||||
*
|
||||
* The port used to connect to the web server. By default this is 80 for HTTP and 443 for HTTPS.
|
||||
* This class never returns -1 for the port: if no port is explicitly specified in the URL then the
|
||||
* scheme's default is used.
|
||||
*
|
||||
* ### Path
|
||||
*
|
||||
* The path identifies a specific resource on the host. Paths have a hierarchical structure like
|
||||
* "/square/okhttp/issues/1486" and decompose into a list of segments like `["square", "okhttp",
|
||||
* "issues", "1486"]`.
|
||||
*
|
||||
* This class offers methods to compose and decompose paths by segment. It composes each path
|
||||
* from a list of segments by alternating between "/" and the encoded segment. For example the
|
||||
* segments `["a", "b"]` build "/a/b" and the segments `["a", "b", ""]` build "/a/b/".
|
||||
*
|
||||
* If a path's last segment is the empty string then the path ends with "/". This class always
|
||||
* builds non-empty paths: if the path is omitted it defaults to "/". The default path's segment
|
||||
* list is a single empty string: `[""]`.
|
||||
*
|
||||
* ### Query
|
||||
*
|
||||
* The query is optional: it can be null, empty, or non-empty. For many HTTP URLs the query string
|
||||
* is subdivided into a collection of name-value parameters. This class offers methods to set the
|
||||
* query as the single string, or as individual name-value parameters. With name-value parameters
|
||||
* the values are optional and names may be repeated.
|
||||
*
|
||||
* ### Fragment
|
||||
*
|
||||
* The fragment is optional: it can be null, empty, or non-empty. Unlike host, port, path, and
|
||||
* query the fragment is not sent to the webserver: it's private to the client.
|
||||
*
|
||||
* ## Encoding
|
||||
*
|
||||
* Each component must be encoded before it is embedded in the complete URL. As we saw above, the
|
||||
* string `cute #puppies` is encoded as `cute%20%23puppies` when used as a query parameter value.
|
||||
*
|
||||
* ### Percent encoding
|
||||
*
|
||||
* Percent encoding replaces a character (like `\ud83c\udf69`) with its UTF-8 hex bytes (like
|
||||
* `%F0%9F%8D%A9`). This approach works for whitespace characters, control characters, non-ASCII
|
||||
* characters, and characters that already have another meaning in a particular context.
|
||||
*
|
||||
* Percent encoding is used in every URL component except for the hostname. But the set of
|
||||
* characters that need to be encoded is different for each component. For example, the path
|
||||
* component must escape all of its `?` characters, otherwise it could be interpreted as the
|
||||
* start of the URL's query. But within the query and fragment components, the `?` character
|
||||
* doesn't delimit anything and doesn't need to be escaped.
|
||||
*
|
||||
* ```java
|
||||
* HttpUrl url = HttpUrl.parse("http://who-let-the-dogs.out").newBuilder()
|
||||
* .addPathSegment("_Who?_")
|
||||
* .query("_Who?_")
|
||||
* .fragment("_Who?_")
|
||||
* .build();
|
||||
* System.out.println(url);
|
||||
* ```
|
||||
*
|
||||
* This prints:
|
||||
*
|
||||
* ```
|
||||
* http://who-let-the-dogs.out/_Who%3F_?_Who?_#_Who?_
|
||||
* ```
|
||||
*
|
||||
* When parsing URLs that lack percent encoding where it is required, this class will percent encode
|
||||
* the offending characters.
|
||||
*
|
||||
* ### IDNA Mapping and Punycode encoding
|
||||
*
|
||||
* Hostnames have different requirements and use a different encoding scheme. It consists of IDNA
|
||||
* mapping and Punycode encoding.
|
||||
*
|
||||
* In order to avoid confusion and discourage phishing attacks, [IDNA Mapping][idna] transforms
|
||||
* names to avoid confusing characters. This includes basic case folding: transforming shouting
|
||||
* `SQUARE.COM` into cool and casual `square.com`. It also handles more exotic characters. For
|
||||
* example, the Unicode trademark sign (™) could be confused for the letters "TM" in
|
||||
* `http://ho™ail.com`. To mitigate this, the single character (™) maps to the string (tm). There
|
||||
* is similar policy for all of the 1.1 million Unicode code points. Note that some code points such
|
||||
* as "\ud83c\udf69" are not mapped and cannot be used in a hostname.
|
||||
*
|
||||
* [Punycode](http://ietf.org/rfc/rfc3492.txt) converts a Unicode string to an ASCII string to make
|
||||
* international domain names work everywhere. For example, "σ" encodes as "xn--4xa". The encoded
|
||||
* string is not human readable, but can be used with classes like [InetAddress] to establish
|
||||
* connections.
|
||||
*
|
||||
* ## Why another URL model?
|
||||
*
|
||||
* Java includes both [java.net.URL][URL] and [java.net.URI][URI]. We offer a new URL
|
||||
* model to address problems that the others don't.
|
||||
*
|
||||
* ### Different URLs should be different
|
||||
*
|
||||
* Although they have different content, `java.net.URL` considers the following two URLs
|
||||
* equal, and the [equals()][Object.equals] method between them returns true:
|
||||
*
|
||||
* * https://example.net/
|
||||
*
|
||||
* * https://example.com/
|
||||
*
|
||||
* This is because those two hosts share the same IP address. This is an old, bad design decision
|
||||
* that makes `java.net.URL` unusable for many things. It shouldn't be used as a [Map] key or in a
|
||||
* [Set]. Doing so is both inefficient because equality may require a DNS lookup, and incorrect
|
||||
* because unequal URLs may be equal because of how they are hosted.
|
||||
*
|
||||
* ### Equal URLs should be equal
|
||||
*
|
||||
* These two URLs are semantically identical, but `java.net.URI` disagrees:
|
||||
*
|
||||
* * http://host:80/
|
||||
*
|
||||
* * http://host
|
||||
*
|
||||
* Both the unnecessary port specification (`:80`) and the absent trailing slash (`/`) cause URI to
|
||||
* bucket the two URLs separately. This harms URI's usefulness in collections. Any application that
|
||||
* stores information-per-URL will need to either canonicalize manually, or suffer unnecessary
|
||||
* redundancy for such URLs.
|
||||
*
|
||||
* Because they don't attempt canonical form, these classes are surprisingly difficult to use
|
||||
* securely. Suppose you're building a webservice that checks that incoming paths are prefixed
|
||||
* "/static/images/" before serving the corresponding assets from the filesystem.
|
||||
*
|
||||
* ```java
|
||||
* String attack = "http://example.com/static/images/../../../../../etc/passwd";
|
||||
* System.out.println(new URL(attack).getPath());
|
||||
* System.out.println(new URI(attack).getPath());
|
||||
* System.out.println(HttpUrl.parse(attack).encodedPath());
|
||||
* ```
|
||||
*
|
||||
* By canonicalizing the input paths, they are complicit in directory traversal attacks. Code that
|
||||
* checks only the path prefix may suffer!
|
||||
*
|
||||
* ```
|
||||
* /static/images/../../../../../etc/passwd
|
||||
* /static/images/../../../../../etc/passwd
|
||||
* /etc/passwd
|
||||
* ```
|
||||
*
|
||||
* ### If it works on the web, it should work in your application
|
||||
*
|
||||
* The `java.net.URI` class is strict around what URLs it accepts. It rejects URLs like
|
||||
* `http://example.com/abc|def` because the `|` character is unsupported. This class is more
|
||||
* forgiving: it will automatically percent-encode the `|'` yielding `http://example.com/abc%7Cdef`.
|
||||
* This kind behavior is consistent with web browsers. `HttpUrl` prefers consistency with major web
|
||||
* browsers over consistency with obsolete specifications.
|
||||
*
|
||||
* ### Paths and Queries should decompose
|
||||
*
|
||||
* Neither of the built-in URL models offer direct access to path segments or query parameters.
|
||||
* Manually using `StringBuilder` to assemble these components is cumbersome: do '+' characters get
|
||||
* silently replaced with spaces? If a query parameter contains a '&', does that get escaped?
|
||||
* By offering methods to read and write individual query parameters directly, application
|
||||
* developers are saved from the hassles of encoding and decoding.
|
||||
*
|
||||
* ### Plus a modern API
|
||||
*
|
||||
* The URL (JDK1.0) and URI (Java 1.4) classes predate builders and instead use telescoping
|
||||
* constructors. For example, there's no API to compose a URI with a custom port without also
|
||||
* providing a query and fragment.
|
||||
*
|
||||
* Instances of [HttpUrl] are well-formed and always have a scheme, host, and path. With
|
||||
* `java.net.URL` it's possible to create an awkward URL like `http:/` with scheme and path but no
|
||||
* hostname. Building APIs that consume such malformed values is difficult!
|
||||
*
|
||||
* This class has a modern API. It avoids punitive checked exceptions: [toHttpUrl] throws
|
||||
* [IllegalArgumentException] on invalid input or [toHttpUrlOrNull] returns null if the input is an
|
||||
* invalid URL. You can even be explicit about whether each component has been encoded already.
|
||||
*
|
||||
* [idna]: http://www.unicode.org/reports/tr46/#ToASCII
|
||||
*/
|
||||
actual class HttpUrl internal actual constructor(
|
||||
actual val scheme: String,
|
||||
actual val username: String,
|
||||
actual val password: String,
|
||||
actual val host: String,
|
||||
actual val port: Int,
|
||||
actual val pathSegments: List<String>,
|
||||
internal actual val queryNamesAndValues: List<String?>?,
|
||||
actual val fragment: String?,
|
||||
internal actual val url: String
|
||||
) {
|
||||
|
||||
actual val isHttps: Boolean
|
||||
get() = commonIsHttps
|
||||
|
||||
actual val encodedUsername: String
|
||||
get() = commonEncodedUsername
|
||||
|
||||
actual val encodedPassword: String
|
||||
get() = commonEncodedPassword
|
||||
|
||||
actual val pathSize: Int
|
||||
get() = commonPathSize
|
||||
|
||||
actual val encodedPath: String
|
||||
get() = commonEncodedPath
|
||||
|
||||
actual val encodedPathSegments: List<String>
|
||||
get() = commonEncodedPathSegments
|
||||
|
||||
actual val encodedQuery: String?
|
||||
get() = commonEncodedQuery
|
||||
|
||||
actual val query: String?
|
||||
get() = commonQuery
|
||||
|
||||
actual val querySize: Int
|
||||
get() = commonQuerySize
|
||||
|
||||
actual fun queryParameter(name: String): String? = commonQueryParameter(name)
|
||||
|
||||
actual val queryParameterNames: Set<String>
|
||||
get() = commonQueryParameterNames
|
||||
|
||||
actual fun queryParameterValues(name: String): List<String?> = commonQueryParameterValues(name)
|
||||
|
||||
actual fun queryParameterName(index: Int): String = commonQueryParameterName(index)
|
||||
|
||||
actual fun queryParameterValue(index: Int): String? = commonQueryParameterValue(index)
|
||||
|
||||
actual val encodedFragment: String?
|
||||
get() = commonEncodedFragment
|
||||
|
||||
actual fun redact(): String = commonRedact()
|
||||
|
||||
actual fun resolve(link: String): HttpUrl? = commonResolve(link)
|
||||
|
||||
actual fun newBuilder(): HttpUrl.Builder = commonNewBuilder()
|
||||
|
||||
actual fun newBuilder(link: String): Builder? = commonNewBuilder(link)
|
||||
|
||||
override fun equals(other: Any?): Boolean = commonEquals(other)
|
||||
|
||||
override fun hashCode(): Int = commonHashCode()
|
||||
|
||||
override fun toString(): String = commonToString()
|
||||
|
||||
actual companion object {
|
||||
actual fun String.toHttpUrl(): HttpUrl = commonToHttpUrl()
|
||||
|
||||
actual fun String.toHttpUrlOrNull(): HttpUrl? = commonToHttpUrlOrNull()
|
||||
|
||||
actual fun defaultPort(scheme: String): Int = CommonHttpUrl.commonDefaultPort(scheme)
|
||||
}
|
||||
|
||||
actual class Builder {
|
||||
internal actual var scheme: String? = null
|
||||
internal actual var encodedUsername = ""
|
||||
internal actual var encodedPassword = ""
|
||||
internal actual var host: String? = null
|
||||
internal actual var port = -1
|
||||
internal actual val encodedPathSegments = mutableListOf<String>("")
|
||||
internal actual var encodedQueryNamesAndValues: MutableList<String?>? = null
|
||||
internal actual var encodedFragment: String? = null
|
||||
|
||||
/**
|
||||
* @param scheme either "http" or "https".
|
||||
*/
|
||||
actual fun scheme(scheme: String) = commonScheme(scheme)
|
||||
|
||||
actual fun username(username: String) = commonUsername(username)
|
||||
|
||||
actual fun encodedUsername(encodedUsername: String) = commonEncodedUsername(encodedUsername)
|
||||
|
||||
actual fun password(password: String) = commonPassword(password)
|
||||
|
||||
actual fun encodedPassword(encodedPassword: String) = commonEncodedPassword(encodedPassword)
|
||||
|
||||
/**
|
||||
* @param host either a regular hostname, International Domain Name, IPv4 address, or IPv6
|
||||
* address.
|
||||
*/
|
||||
actual fun host(host: String) = commonHost(host)
|
||||
|
||||
actual fun port(port: Int) = commonPort(port)
|
||||
|
||||
actual fun addPathSegment(pathSegment: String) = commonAddPathSegment(pathSegment)
|
||||
|
||||
/**
|
||||
* Adds a set of path segments separated by a slash (either `\` or `/`). If `pathSegments`
|
||||
* starts with a slash, the resulting URL will have empty path segment.
|
||||
*/
|
||||
actual fun addPathSegments(pathSegments: String): Builder = commonAddPathSegments(pathSegments)
|
||||
|
||||
actual fun addEncodedPathSegment(encodedPathSegment: String) = commonAddEncodedPathSegment(encodedPathSegment)
|
||||
|
||||
/**
|
||||
* Adds a set of encoded path segments separated by a slash (either `\` or `/`). If
|
||||
* `encodedPathSegments` starts with a slash, the resulting URL will have empty path segment.
|
||||
*/
|
||||
actual fun addEncodedPathSegments(encodedPathSegments: String): Builder =
|
||||
commonAddEncodedPathSegments(encodedPathSegments)
|
||||
|
||||
|
||||
actual fun setPathSegment(index: Int, pathSegment: String) = commonSetPathSegment(index, pathSegment)
|
||||
|
||||
actual fun setEncodedPathSegment(index: Int, encodedPathSegment: String) =
|
||||
commonSetEncodedPathSegment(index, encodedPathSegment)
|
||||
|
||||
actual fun removePathSegment(index: Int) = commonRemovePathSegment(index)
|
||||
|
||||
actual fun encodedPath(encodedPath: String) = commonEncodedPath(encodedPath)
|
||||
|
||||
actual fun query(query: String?) = commonQuery(query)
|
||||
|
||||
actual fun encodedQuery(encodedQuery: String?) = commonEncodedQuery(encodedQuery)
|
||||
|
||||
/** Encodes the query parameter using UTF-8 and adds it to this URL's query string. */
|
||||
actual fun addQueryParameter(name: String, value: String?) = commonAddQueryParameter(name, value)
|
||||
|
||||
/** Adds the pre-encoded query parameter to this URL's query string. */
|
||||
actual fun addEncodedQueryParameter(encodedName: String, encodedValue: String?) =
|
||||
commonAddEncodedQueryParameter(encodedName, encodedValue)
|
||||
|
||||
actual fun setQueryParameter(name: String, value: String?) = commonSetQueryParameter(name, value)
|
||||
|
||||
actual fun setEncodedQueryParameter(encodedName: String, encodedValue: String?) =
|
||||
commonSetEncodedQueryParameter(encodedName, encodedValue)
|
||||
|
||||
actual fun removeAllQueryParameters(name: String) = commonRemoveAllQueryParameters(name)
|
||||
|
||||
actual fun removeAllEncodedQueryParameters(encodedName: String) = commonRemoveAllEncodedQueryParameters(encodedName)
|
||||
|
||||
actual fun fragment(fragment: String?) = commonFragment(fragment)
|
||||
|
||||
actual fun encodedFragment(encodedFragment: String?) = commonEncodedFragment(encodedFragment)
|
||||
|
||||
actual fun build(): HttpUrl = commonBuild()
|
||||
|
||||
override fun toString(): String = commonToString()
|
||||
|
||||
internal actual fun parse(base: HttpUrl?, input: String): Builder = commonParse(base, input)
|
||||
}
|
||||
}
|
@@ -16,6 +16,7 @@
|
||||
package okhttp3
|
||||
|
||||
import kotlin.reflect.KClass
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.internal.canonicalUrl
|
||||
import okhttp3.internal.commonAddHeader
|
||||
import okhttp3.internal.commonCacheControl
|
||||
@@ -33,7 +34,7 @@ import okhttp3.internal.commonRemoveHeader
|
||||
import okhttp3.internal.commonTag
|
||||
|
||||
actual class Request internal actual constructor(builder: Builder) {
|
||||
actual val url: String = checkNotNull(builder.url) { "url == null" }
|
||||
actual val url: HttpUrl = checkNotNull(builder.url) { "url == null" }
|
||||
actual val method: String = builder.method
|
||||
actual val headers: Headers = builder.headers.build()
|
||||
actual val body: RequestBody? = builder.body
|
||||
@@ -42,7 +43,7 @@ actual class Request internal actual constructor(builder: Builder) {
|
||||
internal actual var lazyCacheControl: CacheControl? = null
|
||||
|
||||
actual val isHttps: Boolean
|
||||
get() = url.startsWith("https://")
|
||||
get() = url.isHttps
|
||||
|
||||
/**
|
||||
* Constructs a new request.
|
||||
@@ -108,7 +109,7 @@ actual class Request internal actual constructor(builder: Builder) {
|
||||
}
|
||||
|
||||
actual open class Builder {
|
||||
internal actual var url: String? = null
|
||||
internal actual var url: HttpUrl? = null
|
||||
internal actual var method: String
|
||||
internal actual var headers: Headers.Builder
|
||||
internal actual var body: RequestBody? = null
|
||||
@@ -133,12 +134,12 @@ actual class Request internal actual constructor(builder: Builder) {
|
||||
this.headers = request.headers.newBuilder()
|
||||
}
|
||||
|
||||
// open fun url(url: HttpUrl): Builder = apply {
|
||||
// this.url = url
|
||||
// }
|
||||
actual open fun url(url: HttpUrl): Builder = apply {
|
||||
this.url = url
|
||||
}
|
||||
|
||||
actual open fun url(url: String): Builder = apply {
|
||||
this.url = canonicalUrl(url)
|
||||
url(canonicalUrl(url).toHttpUrl())
|
||||
}
|
||||
|
||||
actual open fun header(name: String, value: String) = commonHeader(name, value)
|
||||
|
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package okhttp3.internal
|
||||
|
||||
actual fun String.toCanonicalHost(): String? {
|
||||
val host: String = this
|
||||
|
||||
// If the input contains a :, it’s an IPv6 address.
|
||||
if (":" in host) {
|
||||
// If the input is encased in square braces "[...]", drop 'em.
|
||||
val inetAddressByteArray = (if (host.startsWith("[") && host.endsWith("]")) {
|
||||
decodeIpv6(host, 1, host.length - 1)
|
||||
} else {
|
||||
decodeIpv6(host, 0, host.length)
|
||||
}) ?: return null
|
||||
// TODO implement properly
|
||||
return inet6AddressToAscii(inetAddressByteArray)
|
||||
}
|
||||
|
||||
try {
|
||||
val result = idnToAscii(host)
|
||||
if (result.isEmpty()) return null
|
||||
|
||||
return if (result.containsInvalidHostnameAsciiCodes()) {
|
||||
// The IDN ToASCII result contains illegal characters.
|
||||
null
|
||||
} else if (result.containsInvalidLabelLengths()) {
|
||||
// The IDN ToASCII result contains invalid labels.
|
||||
null
|
||||
} else {
|
||||
result
|
||||
}
|
||||
} catch (_: IllegalArgumentException) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
internal fun inet4AddressToAscii(address: ByteArray): String {
|
||||
return address.joinToString(".")
|
||||
}
|
||||
|
||||
fun idnToAscii(host: String): String {
|
||||
// TODO implement properly
|
||||
return host.lowercase()
|
||||
}
|
148
okhttp/src/nonJvmMain/kotlin/okhttp3/internal/-HttpUrlNonJvm.kt
Normal file
148
okhttp/src/nonJvmMain/kotlin/okhttp3/internal/-HttpUrlNonJvm.kt
Normal file
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package okhttp3.internal
|
||||
|
||||
import okhttp3.internal.CommonHttpUrl.isPercentEncoded
|
||||
import okhttp3.internal.NonJvmHttpUrl.writeCanonicalized
|
||||
import okio.Buffer
|
||||
|
||||
internal object NonJvmHttpUrl {
|
||||
internal fun Buffer.writeCanonicalized(
|
||||
input: String,
|
||||
pos: Int,
|
||||
limit: Int,
|
||||
encodeSet: String,
|
||||
alreadyEncoded: Boolean,
|
||||
strict: Boolean,
|
||||
plusIsSpace: Boolean,
|
||||
unicodeAllowed: Boolean,
|
||||
) {
|
||||
var encodedCharBuffer: Buffer? = null // Lazily allocated.
|
||||
var codePoint: Int
|
||||
var i = pos
|
||||
while (i < limit) {
|
||||
codePoint = input[i].code
|
||||
if (alreadyEncoded && (codePoint == '\t'.code || codePoint == '\n'.code ||
|
||||
codePoint == '\u000c'.code || codePoint == '\r'.code)) {
|
||||
// Skip this character.
|
||||
} else if (codePoint == ' '.code && encodeSet === CommonHttpUrl.FORM_ENCODE_SET) {
|
||||
// Encode ' ' as '+'.
|
||||
writeUtf8("+")
|
||||
} else if (codePoint == '+'.code && plusIsSpace) {
|
||||
// Encode '+' as '%2B' since we permit ' ' to be encoded as either '+' or '%20'.
|
||||
writeUtf8(if (alreadyEncoded) "+" else "%2B")
|
||||
} else if (codePoint < 0x20 ||
|
||||
codePoint == 0x7f ||
|
||||
codePoint >= 0x80 && !unicodeAllowed ||
|
||||
codePoint.toChar() in encodeSet ||
|
||||
codePoint == '%'.code &&
|
||||
(!alreadyEncoded || strict && !input.isPercentEncoded(i, limit))) {
|
||||
// Percent encode this character.
|
||||
if (encodedCharBuffer == null) {
|
||||
encodedCharBuffer = Buffer()
|
||||
}
|
||||
|
||||
encodedCharBuffer.writeUtf8CodePoint(codePoint)
|
||||
|
||||
while (!encodedCharBuffer.exhausted()) {
|
||||
val b = encodedCharBuffer.readByte().toInt() and 0xff
|
||||
writeByte('%'.code)
|
||||
writeByte(CommonHttpUrl.HEX_DIGITS[b shr 4 and 0xf].code)
|
||||
writeByte(CommonHttpUrl.HEX_DIGITS[b and 0xf].code)
|
||||
}
|
||||
} else {
|
||||
// This character doesn't need encoding. Just copy it over.
|
||||
writeUtf8CodePoint(codePoint)
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal actual object HttpUrlCommon {
|
||||
internal actual fun Buffer.writePercentDecoded(
|
||||
encoded: String,
|
||||
pos: Int,
|
||||
limit: Int,
|
||||
plusIsSpace: Boolean
|
||||
) {
|
||||
var codePoint: Int
|
||||
var i = pos
|
||||
while (i < limit) {
|
||||
codePoint = encoded.get(i).code
|
||||
if (codePoint == '%'.code && i + 2 < limit) {
|
||||
val d1 = encoded[i + 1].parseHexDigit()
|
||||
val d2 = encoded[i + 2].parseHexDigit()
|
||||
if (d1 != -1 && d2 != -1) {
|
||||
writeByte((d1 shl 4) + d2)
|
||||
i += 2
|
||||
i += 1
|
||||
continue
|
||||
}
|
||||
} else if (codePoint == '+'.code && plusIsSpace) {
|
||||
writeByte(' '.code)
|
||||
i++
|
||||
continue
|
||||
}
|
||||
writeUtf8CodePoint(codePoint)
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
|
||||
internal actual fun String.canonicalize(
|
||||
pos: Int,
|
||||
limit: Int,
|
||||
encodeSet: String,
|
||||
alreadyEncoded: Boolean,
|
||||
strict: Boolean,
|
||||
plusIsSpace: Boolean,
|
||||
unicodeAllowed: Boolean,
|
||||
): String {
|
||||
var codePoint: Int
|
||||
var i = pos
|
||||
while (i < limit) {
|
||||
codePoint = this[i].code
|
||||
if (codePoint < 0x20 ||
|
||||
codePoint == 0x7f ||
|
||||
codePoint >= 0x80 && !unicodeAllowed ||
|
||||
codePoint.toChar() in encodeSet ||
|
||||
codePoint == '%'.code &&
|
||||
(!alreadyEncoded || strict && !isPercentEncoded(i, limit)) ||
|
||||
codePoint == '+'.code && plusIsSpace
|
||||
) {
|
||||
// Slow path: the character at i requires encoding!
|
||||
val out = Buffer()
|
||||
out.writeUtf8(this, pos, i)
|
||||
out.writeCanonicalized(
|
||||
input = this,
|
||||
pos = i,
|
||||
limit = limit,
|
||||
encodeSet = encodeSet,
|
||||
alreadyEncoded = alreadyEncoded,
|
||||
strict = strict,
|
||||
plusIsSpace = plusIsSpace,
|
||||
unicodeAllowed = unicodeAllowed,
|
||||
)
|
||||
return out.readUtf8()
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
|
||||
// Fast path: no characters in [pos..limit) required encoding.
|
||||
return substring(pos, limit)
|
||||
}
|
||||
}
|
||||
|
@@ -15,5 +15,3 @@
|
||||
*/
|
||||
|
||||
package okhttp3.internal
|
||||
|
||||
actual typealias HttpUrlRepresentation = String
|
||||
|
Reference in New Issue
Block a user