From f9a70f69b00340f2bf8e9b487b6429b0ad53947f Mon Sep 17 00:00:00 2001 From: beatrausch Date: Sat, 5 Mar 2022 20:15:33 +0100 Subject: [PATCH 1/4] okhttp: forked required files to make okhttp dep compile only --- .../java/io/grpc/okhttp/internal/Headers.java | 318 ++++ .../java/io/grpc/okhttp/internal/HttpUrl.java | 1615 +++++++++++++++++ .../java/io/grpc/okhttp/internal/Request.java | 285 +++ .../io/grpc/okhttp/internal/StatusLine.java | 94 + 4 files changed, 2312 insertions(+) create mode 100644 okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/Headers.java create mode 100644 okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/HttpUrl.java create mode 100644 okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/Request.java create mode 100644 okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/StatusLine.java diff --git a/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/Headers.java b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/Headers.java new file mode 100644 index 00000000000..a81e67472ae --- /dev/null +++ b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/Headers.java @@ -0,0 +1,318 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ +/* + * Forked from OkHttp 2.7.0 + */ + +package io.grpc.okhttp.internal; + +import com.squareup.okhttp.internal.http.HttpDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +/** + * The header fields of a single HTTP message. Values are uninterpreted strings; + * use {@code Request} and {@code Response} for interpreted headers. This class + * maintains the order of the header fields within the HTTP message. + * + *

This class tracks header values line-by-line. A field with multiple comma- + * separated values on the same line will be treated as a field with a single + * value by this class. It is the caller's responsibility to detect and split + * on commas if their field permits multiple values. This simplifies use of + * single-valued fields whose values routinely contain commas, such as cookies + * or dates. + * + *

This class trims whitespace from values. It never returns values with + * leading or trailing whitespace. + * + *

Instances of this class are immutable. Use {@link Builder} to create + * instances. + */ +public final class Headers { + private final String[] namesAndValues; + + private Headers(Builder builder) { + this.namesAndValues = builder.namesAndValues.toArray(new String[builder.namesAndValues.size()]); + } + + private Headers(String[] namesAndValues) { + this.namesAndValues = namesAndValues; + } + + /** Returns the last value corresponding to the specified field, or null. */ + public String get(String name) { + return get(namesAndValues, name); + } + + /** + * Returns the last value corresponding to the specified field parsed as an + * HTTP date, or null if either the field is absent or cannot be parsed as a + * date. + */ + public Date getDate(String name) { + String value = get(name); + return value != null ? HttpDate.parse(value) : null; + } + + /** Returns the number of field values. */ + public int size() { + return namesAndValues.length / 2; + } + + /** Returns the field at {@code position} or null if that is out of range. */ + public String name(int index) { + int nameIndex = index * 2; + if (nameIndex < 0 || nameIndex >= namesAndValues.length) { + return null; + } + return namesAndValues[nameIndex]; + } + + /** Returns the value at {@code index} or null if that is out of range. */ + public String value(int index) { + int valueIndex = index * 2 + 1; + if (valueIndex < 0 || valueIndex >= namesAndValues.length) { + return null; + } + return namesAndValues[valueIndex]; + } + + /** Returns an immutable case-insensitive set of header names. */ + public Set names() { + TreeSet result = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + for (int i = 0, size = size(); i < size; i++) { + result.add(name(i)); + } + return Collections.unmodifiableSet(result); + } + + /** Returns an immutable list of the header values for {@code name}. */ + public List values(String name) { + List result = null; + for (int i = 0, size = size(); i < size; i++) { + if (name.equalsIgnoreCase(name(i))) { + if (result == null) result = new ArrayList<>(2); + result.add(value(i)); + } + } + return result != null + ? Collections.unmodifiableList(result) + : Collections.emptyList(); + } + + public Builder newBuilder() { + Builder result = new Builder(); + Collections.addAll(result.namesAndValues, namesAndValues); + return result; + } + + @Override public String toString() { + StringBuilder result = new StringBuilder(); + for (int i = 0, size = size(); i < size; i++) { + result.append(name(i)).append(": ").append(value(i)).append("\n"); + } + return result.toString(); + } + + public Map> toMultimap() { + Map> result = new LinkedHashMap>(); + for (int i = 0, size = size(); i < size; i++) { + String name = name(i); + List values = result.get(name); + if (values == null) { + values = new ArrayList<>(2); + result.put(name, values); + } + values.add(value(i)); + } + return result; + } + + private static String get(String[] namesAndValues, String name) { + for (int i = namesAndValues.length - 2; i >= 0; i -= 2) { + if (name.equalsIgnoreCase(namesAndValues[i])) { + return namesAndValues[i + 1]; + } + } + return null; + } + + /** + * Returns headers for the alternating header names and values. There must be + * an even number of arguments, and they must alternate between header names + * and values. + */ + public static Headers of(String... namesAndValues) { + if (namesAndValues == null || namesAndValues.length % 2 != 0) { + throw new IllegalArgumentException("Expected alternating header names and values"); + } + + // Make a defensive copy and clean it up. + namesAndValues = namesAndValues.clone(); + for (int i = 0; i < namesAndValues.length; i++) { + if (namesAndValues[i] == null) throw new IllegalArgumentException("Headers cannot be null"); + namesAndValues[i] = namesAndValues[i].trim(); + } + + // Check for malformed headers. + for (int i = 0; i < namesAndValues.length; i += 2) { + String name = namesAndValues[i]; + String value = namesAndValues[i + 1]; + if (name.length() == 0 || name.indexOf('\0') != -1 || value.indexOf('\0') != -1) { + throw new IllegalArgumentException("Unexpected header: " + name + ": " + value); + } + } + + return new Headers(namesAndValues); + } + + /** + * Returns headers for the header names and values in the {@link Map}. + */ + public static Headers of(Map headers) { + if (headers == null) { + throw new IllegalArgumentException("Expected map with header names and values"); + } + + // Make a defensive copy and clean it up. + String[] namesAndValues = new String[headers.size() * 2]; + int i = 0; + for (Map.Entry header : headers.entrySet()) { + if (header.getKey() == null || header.getValue() == null) { + throw new IllegalArgumentException("Headers cannot be null"); + } + String name = header.getKey().trim(); + String value = header.getValue().trim(); + if (name.length() == 0 || name.indexOf('\0') != -1 || value.indexOf('\0') != -1) { + throw new IllegalArgumentException("Unexpected header: " + name + ": " + value); + } + namesAndValues[i] = name; + namesAndValues[i + 1] = value; + i += 2; + } + + return new Headers(namesAndValues); + } + + public static final class Builder { + private final List namesAndValues = new ArrayList<>(20); + + /** + * Add a header line without any validation. Only appropriate for headers from the remote peer + * or cache. + */ + Builder addLenient(String line) { + int index = line.indexOf(":", 1); + if (index != -1) { + return addLenient(line.substring(0, index), line.substring(index + 1)); + } else if (line.startsWith(":")) { + // Work around empty header names and header names that start with a + // colon (created by old broken SPDY versions of the response cache). + return addLenient("", line.substring(1)); // Empty header name. + } else { + return addLenient("", line); // No header name. + } + } + + /** Add an header line containing a field name, a literal colon, and a value. */ + public Builder add(String line) { + int index = line.indexOf(":"); + if (index == -1) { + throw new IllegalArgumentException("Unexpected header: " + line); + } + return add(line.substring(0, index).trim(), line.substring(index + 1)); + } + + /** Add a field with the specified value. */ + public Builder add(String name, String value) { + checkNameAndValue(name, value); + return addLenient(name, value); + } + + /** + * Add a field with the specified value without any validation. Only + * appropriate for headers from the remote peer or cache. + */ + Builder addLenient(String name, String value) { + namesAndValues.add(name); + namesAndValues.add(value.trim()); + return this; + } + + public Builder removeAll(String name) { + for (int i = 0; i < namesAndValues.size(); i += 2) { + if (name.equalsIgnoreCase(namesAndValues.get(i))) { + namesAndValues.remove(i); // name + namesAndValues.remove(i); // value + i -= 2; + } + } + return this; + } + + /** + * Set a field with the specified value. If the field is not found, it is + * added. If the field is found, the existing values are replaced. + */ + public Builder set(String name, String value) { + checkNameAndValue(name, value); + removeAll(name); + addLenient(name, value); + return this; + } + + private void checkNameAndValue(String name, String value) { + if (name == null) throw new IllegalArgumentException("name == null"); + if (name.isEmpty()) throw new IllegalArgumentException("name is empty"); + for (int i = 0, length = name.length(); i < length; i++) { + char c = name.charAt(i); + if (c <= '\u001f' || c >= '\u007f') { + throw new IllegalArgumentException(String.format( + "Unexpected char %#04x at %d in header name: %s", (int) c, i, name)); + } + } + if (value == null) throw new IllegalArgumentException("value == null"); + for (int i = 0, length = value.length(); i < length; i++) { + char c = value.charAt(i); + if (c <= '\u001f' || c >= '\u007f') { + throw new IllegalArgumentException(String.format( + "Unexpected char %#04x at %d in header value: %s", (int) c, i, value)); + } + } + } + + /** Equivalent to {@code build().get(name)}, but potentially faster. */ + public String get(String name) { + for (int i = namesAndValues.size() - 2; i >= 0; i -= 2) { + if (name.equalsIgnoreCase(namesAndValues.get(i))) { + return namesAndValues.get(i + 1); + } + } + return null; + } + + public Headers build() { + return new Headers(this); + } + } +} diff --git a/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/HttpUrl.java b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/HttpUrl.java new file mode 100644 index 00000000000..b0ed5c80438 --- /dev/null +++ b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/HttpUrl.java @@ -0,0 +1,1615 @@ +/* + * 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. + */ +/* + * Forked from OkHttp 2.7.0 + */ + +package io.grpc.okhttp.internal; + +import java.net.IDN; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import okio.Buffer; + +/** + * A uniform resource locator (URL) with a scheme of either {@code http} or {@code https}. Use this + * class to compose and decompose Internet addresses. For example, this code will compose and print + * a URL for Google search:

   {@code
+ *
+ *   HttpUrl url = new HttpUrl.Builder()
+ *       .scheme("https")
+ *       .host("www.google.com")
+ *       .addPathSegment("search")
+ *       .addQueryParameter("q", "polar bears")
+ *       .build();
+ *   System.out.println(url);
+ * }
+ * + * which prints:
   {@code
+ *
+ *     https://www.google.com/search?q=polar%20bears
+ * }
+ * + * As another example, this code prints the human-readable query parameters of a Twitter search: + *
   {@code
+ *
+ *   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:
   {@code
+ *
+ *   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:
   {@code
+ *
+ *   HttpUrl base = HttpUrl.parse("https://www.youtube.com/user/WatchTheDaily/videos");
+ *   HttpUrl link = base.resolve("../../watch?v=cbP2N1BQdYc");
+ *   System.out.println(link);
+ * }
+ * + * which prints:
   {@code
+ *
+ *   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 ({@code mailto}, {@code file}, {@code + * ftp}), this class only supports {@code http} and {@code https}. Use {@link URI java.net.URI} for + * URLs with arbitrary schemes. + * + *

Username and Password

+ * Username and password are either present, or the empty string {@code ""} 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 + * {@code square.com} or {@code localhost}, an IPv4 address like {@code 192.168.0.1}, or an IPv6 + * address like {@code ::1}. + * + *

Usually a webserver is reachable with multiple identifiers: its IP addresses, registered + * domain names, and even {@code localhost} when connecting from the server itself. Each of a + * webserver's names is a distinct URL and they are not interchangeable. For example, even if + * {@code http://square.github.io/dagger} and {@code 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 webserver. 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". Each path segment is prefixed with "/". This class offers methods + * to compose and decompose paths by segment. 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 "/", which is a path whose only segment is the 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 {@code cute #puppies} is encoded as {@code cute%20%23puppies} when used as a query + * parameter value. + * + *

Percent encoding

+ * Percent encoding replaces a character (like {@code \ud83c\udf69}) with its UTF-8 hex bytes (like + * {@code %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 {@code ?} characters, otherwise it could be interpreted as the + * start of the URL's query. But within the query and fragment components, the {@code ?} character + * doesn't delimit anything and doesn't need to be escaped.

   {@code
+ *
+ *   HttpUrl url = HttpUrl.parse("http://who-let-the-dogs.out").newBuilder()
+ *       .addPathSegment("_Who?_")
+ *       .query("_Who?_")
+ *       .fragment("_Who?_")
+ *       .build();
+ *   System.out.println(url);
+ * }
+ * + * This prints:
   {@code
+ *
+ *   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 transforms names to avoid + * confusing characters. This includes basic case folding: transforming shouting {@code SQUARE.COM} + * into cool and casual {@code square.com}. It also handles more exotic characters. For example, the + * Unicode trademark sign (™) could be confused for the letters "TM" in {@code http://ho™mail.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 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 {@link + * InetAddress} to establish connections. + * + *

Why another URL model?

+ * Java includes both {@link URL java.net.URL} and {@link URI java.net.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, {@code java.net.URL} considers the following two URLs + * equal, and the {@link Object#equals equals()} method between them returns true: + * + * This is because those two hosts share the same IP address. This is an old, bad design decision + * that makes {@code java.net.URL} unusable for many things. It shouldn't be used as a {@link + * java.util.Map Map} key or in a {@link 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 {@code java.net.URI} disagrees: + * + * Both the unnecessary port specification ({@code :80}) and the absent trailing slash ({@code /}) + * 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.

   {@code
+ *
+ *   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).path());
+ * }
+ * + * By canonicalizing the input paths, they are complicit in directory traversal attacks. Code that + * checks only the path prefix may suffer! + *
   {@code
+ *
+ *    /static/images/../../../../../etc/passwd
+ *    /static/images/../../../../../etc/passwd
+ *    /etc/passwd
+ * }
+ * + *

If it works on the web, it should work in your application

+ * The {@code 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. {@code 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 {@code 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 {@link HttpUrl} are well-formed and always have a scheme, host, and path. With + * {@code java.net.URL} it's possible to create an awkward URL like {@code 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: {@link #parse parse()} + * returns null if the input is an invalid URL. You can even be explicit about whether each + * component has been encoded already. + */ +public final class HttpUrl { + private static final char[] HEX_DIGITS = + { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + static final String USERNAME_ENCODE_SET = " \"':;<=>@[]^`{}|/\\?#"; + static final String PASSWORD_ENCODE_SET = " \"':;<=>@[]^`{}|/\\?#"; + static final String PATH_SEGMENT_ENCODE_SET = " \"<>^`{}|/\\?#"; + static final String PATH_SEGMENT_ENCODE_SET_URI = "[]"; + static final String QUERY_ENCODE_SET = " \"'<>#"; + static final String QUERY_COMPONENT_ENCODE_SET = " \"'<>#&="; + static final String QUERY_COMPONENT_ENCODE_SET_URI = "\\^`{|}"; + static final String FORM_ENCODE_SET = " \"':;<=>@[]^`{}|/\\?#&!$(),~"; + static final String FRAGMENT_ENCODE_SET = ""; + static final String FRAGMENT_ENCODE_SET_URI = " \"#<>\\^`{|}"; + + /** Either "http" or "https". */ + private final String scheme; + + /** Decoded username. */ + private final String username; + + /** Decoded password. */ + private final String password; + + /** Canonical hostname. */ + private final String host; + + /** Either 80, 443 or a user-specified port. In range [1..65535]. */ + private final int port; + + /** + * A list of canonical path segments. This list always contains at least one element, which may + * be the empty string. Each segment is formatted with a leading '/', so if path segments were + * ["a", "b", ""], then the encoded path would be "/a/b/". + */ + private final List pathSegments; + + /** + * Alternating, decoded query names and values, or null for no query. Names may be empty or + * non-empty, but never null. Values are null if the name has no corresponding '=' separator, or + * empty, or non-empty. + */ + private final List queryNamesAndValues; + + /** Decoded fragment. */ + private final String fragment; + + /** Canonical URL. */ + private final String url; + + private HttpUrl(Builder builder) { + this.scheme = builder.scheme; + this.username = percentDecode(builder.encodedUsername, false); + this.password = percentDecode(builder.encodedPassword, false); + this.host = builder.host; + this.port = builder.effectivePort(); + this.pathSegments = percentDecode(builder.encodedPathSegments, false); + this.queryNamesAndValues = builder.encodedQueryNamesAndValues != null + ? percentDecode(builder.encodedQueryNamesAndValues, true) + : null; + this.fragment = builder.encodedFragment != null + ? percentDecode(builder.encodedFragment, false) + : null; + this.url = builder.toString(); + } + + /** Returns this URL as a {@link URL java.net.URL}. */ + public URL url() { + try { + return new URL(url); + } catch (MalformedURLException e) { + throw new RuntimeException(e); // Unexpected! + } + } + + /** + * Returns this URL as a {@link URI java.net.URI}. Because {@code URI} forbids certain characters + * like {@code [} and {@code |}, the returned URI may escape more characters than this URL. + * + *

This method throws an unchecked {@link IllegalStateException} if it cannot be converted to a + * URI even after escaping forbidden characters. In particular, URLs that contain malformed + * percent escapes like {@code http://host/%xx} will trigger this exception. + */ + public URI uri() { + try { + String uri = newBuilder().reencodeForUri().toString(); + return new URI(uri); + } catch (URISyntaxException e) { + throw new IllegalStateException("not valid as a java.net.URI: " + url); + } + } + + /** Returns either "http" or "https". */ + public String scheme() { + return scheme; + } + + public boolean isHttps() { + return scheme.equals("https"); + } + + /** Returns the username, or an empty string if none is set. */ + public String encodedUsername() { + if (username.isEmpty()) return ""; + int usernameStart = scheme.length() + 3; // "://".length() == 3. + int usernameEnd = delimiterOffset(url, usernameStart, url.length(), ":@"); + return url.substring(usernameStart, usernameEnd); + } + + public String username() { + return username; + } + + /** Returns the password, or an empty string if none is set. */ + public String encodedPassword() { + if (password.isEmpty()) return ""; + int passwordStart = url.indexOf(':', scheme.length() + 3) + 1; + int passwordEnd = url.indexOf('@'); + return url.substring(passwordStart, passwordEnd); + } + + /** Returns the decoded password, or an empty string if none is present. */ + public String password() { + return password; + } + + /** + * Returns the host address suitable for use with {@link InetAddress#getAllByName(String)}. May + * be: + *

+ */ + public String host() { + return host; + } + + /** + * Returns the explicitly-specified port if one was provided, or the default port for this URL's + * scheme. For example, this returns 8443 for {@code https://square.com:8443/} and 443 for {@code + * https://square.com/}. The result is in {@code [1..65535]}. + */ + public int port() { + return port; + } + + /** + * Returns 80 if {@code scheme.equals("http")}, 443 if {@code scheme.equals("https")} and -1 + * otherwise. + */ + public static int defaultPort(String scheme) { + if (scheme.equals("http")) { + return 80; + } else if (scheme.equals("https")) { + return 443; + } else { + return -1; + } + } + + public int pathSize() { + return pathSegments.size(); + } + + /** + * Returns the entire path of this URL, encoded for use in HTTP resource resolution. The + * returned path is always nonempty and is prefixed with {@code /}. + */ + public String encodedPath() { + int pathStart = url.indexOf('/', scheme.length() + 3); // "://".length() == 3. + int pathEnd = delimiterOffset(url, pathStart, url.length(), "?#"); + return url.substring(pathStart, pathEnd); + } + + static void pathSegmentsToString(StringBuilder out, List pathSegments) { + for (int i = 0, size = pathSegments.size(); i < size; i++) { + out.append('/'); + out.append(pathSegments.get(i)); + } + } + + public List encodedPathSegments() { + int pathStart = url.indexOf('/', scheme.length() + 3); + int pathEnd = delimiterOffset(url, pathStart, url.length(), "?#"); + List result = new ArrayList<>(); + for (int i = pathStart; i < pathEnd; ) { + i++; // Skip the '/'. + int segmentEnd = delimiterOffset(url, i, pathEnd, "/"); + result.add(url.substring(i, segmentEnd)); + i = segmentEnd; + } + return result; + } + + public List pathSegments() { + return pathSegments; + } + + /** + * Returns the query of this URL, encoded for use in HTTP resource resolution. The returned string + * may be null (for URLs with no query), empty (for URLs with an empty query) or non-empty (all + * other URLs). + */ + public String encodedQuery() { + if (queryNamesAndValues == null) return null; // No query. + int queryStart = url.indexOf('?') + 1; + int queryEnd = delimiterOffset(url, queryStart + 1, url.length(), "#"); + return url.substring(queryStart, queryEnd); + } + + static void namesAndValuesToQueryString(StringBuilder out, List namesAndValues) { + for (int i = 0, size = namesAndValues.size(); i < size; i += 2) { + String name = namesAndValues.get(i); + String value = namesAndValues.get(i + 1); + if (i > 0) out.append('&'); + out.append(name); + if (value != null) { + out.append('='); + out.append(value); + } + } + } + + /** + * Cuts {@code encodedQuery} up into alternating parameter names and values. This divides a + * query string like {@code subject=math&easy&problem=5-2=3} into the list {@code ["subject", + * "math", "easy", null, "problem", "5-2=3"]}. Note that values may be null and may contain + * '=' characters. + */ + static List queryStringToNamesAndValues(String encodedQuery) { + List result = new ArrayList<>(); + for (int pos = 0; pos <= encodedQuery.length(); ) { + int ampersandOffset = encodedQuery.indexOf('&', pos); + if (ampersandOffset == -1) ampersandOffset = encodedQuery.length(); + + int equalsOffset = encodedQuery.indexOf('=', pos); + if (equalsOffset == -1 || equalsOffset > ampersandOffset) { + result.add(encodedQuery.substring(pos, ampersandOffset)); + result.add(null); // No value for this name. + } else { + result.add(encodedQuery.substring(pos, equalsOffset)); + result.add(encodedQuery.substring(equalsOffset + 1, ampersandOffset)); + } + pos = ampersandOffset + 1; + } + return result; + } + + public String query() { + if (queryNamesAndValues == null) return null; // No query. + StringBuilder result = new StringBuilder(); + namesAndValuesToQueryString(result, queryNamesAndValues); + return result.toString(); + } + + public int querySize() { + return queryNamesAndValues != null ? queryNamesAndValues.size() / 2 : 0; + } + + /** + * Returns the first query parameter named {@code name} decoded using UTF-8, or null if there is + * no such query parameter. + */ + public String queryParameter(String name) { + if (queryNamesAndValues == null) return null; + for (int i = 0, size = queryNamesAndValues.size(); i < size; i += 2) { + if (name.equals(queryNamesAndValues.get(i))) { + return queryNamesAndValues.get(i + 1); + } + } + return null; + } + + public Set queryParameterNames() { + if (queryNamesAndValues == null) return Collections.emptySet(); + Set result = new LinkedHashSet<>(); + for (int i = 0, size = queryNamesAndValues.size(); i < size; i += 2) { + result.add(queryNamesAndValues.get(i)); + } + return Collections.unmodifiableSet(result); + } + + public List queryParameterValues(String name) { + if (queryNamesAndValues == null) return Collections.emptyList(); + List result = new ArrayList<>(); + for (int i = 0, size = queryNamesAndValues.size(); i < size; i += 2) { + if (name.equals(queryNamesAndValues.get(i))) { + result.add(queryNamesAndValues.get(i + 1)); + } + } + return Collections.unmodifiableList(result); + } + + public String queryParameterName(int index) { + return queryNamesAndValues.get(index * 2); + } + + public String queryParameterValue(int index) { + return queryNamesAndValues.get(index * 2 + 1); + } + + public String encodedFragment() { + if (fragment == null) return null; + int fragmentStart = url.indexOf('#') + 1; + return url.substring(fragmentStart); + } + + public String fragment() { + return fragment; + } + + /** Returns the URL that would be retrieved by following {@code link} from this URL. */ + public HttpUrl resolve(String link) { + Builder builder = new Builder(); + Builder.ParseResult result = builder.parse(this, link); + return result == Builder.ParseResult.SUCCESS ? builder.build() : null; + } + + public Builder newBuilder() { + Builder result = new 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 = port != defaultPort(scheme) ? port : -1; + result.encodedPathSegments.clear(); + result.encodedPathSegments.addAll(encodedPathSegments()); + result.encodedQuery(encodedQuery()); + result.encodedFragment = encodedFragment(); + return result; + } + + /** + * Returns a new {@code HttpUrl} representing {@code url} if it is a well-formed HTTP or HTTPS + * URL, or null if it isn't. + */ + public static HttpUrl parse(String url) { + Builder builder = new Builder(); + Builder.ParseResult result = builder.parse(null, url); + return result == Builder.ParseResult.SUCCESS ? builder.build() : null; + } + + /** + * Returns an {@link HttpUrl} for {@code url} if its protocol is {@code http} or {@code https}, or + * null if it has any other protocol. + */ + public static HttpUrl get(URL url) { + return parse(url.toString()); + } + + /** + * Returns a new {@code HttpUrl} representing {@code url} if it is a well-formed HTTP or HTTPS + * URL, or throws an exception if it isn't. + * + * @throws MalformedURLException if there was a non-host related URL issue + * @throws UnknownHostException if the host was invalid + */ + static HttpUrl getChecked(String url) throws MalformedURLException, UnknownHostException { + Builder builder = new Builder(); + Builder.ParseResult result = builder.parse(null, url); + switch (result) { + case SUCCESS: + return builder.build(); + case INVALID_HOST: + throw new UnknownHostException("Invalid host: " + url); + case UNSUPPORTED_SCHEME: + case MISSING_SCHEME: + case INVALID_PORT: + default: + throw new MalformedURLException("Invalid URL: " + result + " for " + url); + } + } + + public static HttpUrl get(URI uri) { + return parse(uri.toString()); + } + + @Override public boolean equals(Object o) { + return o instanceof HttpUrl && ((HttpUrl) o).url.equals(url); + } + + @Override public int hashCode() { + return url.hashCode(); + } + + @Override public String toString() { + return url; + } + + public static final class Builder { + String scheme; + String encodedUsername = ""; + String encodedPassword = ""; + String host; + int port = -1; + final List encodedPathSegments = new ArrayList<>(); + List encodedQueryNamesAndValues; + String encodedFragment; + + public Builder() { + encodedPathSegments.add(""); // The default path is '/' which needs a trailing space. + } + + public Builder scheme(String scheme) { + if (scheme == null) { + throw new IllegalArgumentException("scheme == null"); + } else if (scheme.equalsIgnoreCase("http")) { + this.scheme = "http"; + } else if (scheme.equalsIgnoreCase("https")) { + this.scheme = "https"; + } else { + throw new IllegalArgumentException("unexpected scheme: " + scheme); + } + return this; + } + + public Builder username(String username) { + if (username == null) throw new IllegalArgumentException("username == null"); + this.encodedUsername = canonicalize(username, USERNAME_ENCODE_SET, false, false, true); + return this; + } + + public Builder encodedUsername(String encodedUsername) { + if (encodedUsername == null) throw new IllegalArgumentException("encodedUsername == null"); + this.encodedUsername = canonicalize(encodedUsername, USERNAME_ENCODE_SET, true, false, true); + return this; + } + + public Builder password(String password) { + if (password == null) throw new IllegalArgumentException("password == null"); + this.encodedPassword = canonicalize(password, PASSWORD_ENCODE_SET, false, false, true); + return this; + } + + public Builder encodedPassword(String encodedPassword) { + if (encodedPassword == null) throw new IllegalArgumentException("encodedPassword == null"); + this.encodedPassword = canonicalize(encodedPassword, PASSWORD_ENCODE_SET, true, false, true); + return this; + } + + /** + * @param host either a regular hostname, International Domain Name, IPv4 address, or IPv6 + * address. + */ + public Builder host(String host) { + if (host == null) throw new IllegalArgumentException("host == null"); + String encoded = canonicalizeHost(host, 0, host.length()); + if (encoded == null) throw new IllegalArgumentException("unexpected host: " + host); + this.host = encoded; + return this; + } + + public Builder port(int port) { + if (port <= 0 || port > 65535) throw new IllegalArgumentException("unexpected port: " + port); + this.port = port; + return this; + } + + int effectivePort() { + return port != -1 ? port : defaultPort(scheme); + } + + public Builder addPathSegment(String pathSegment) { + if (pathSegment == null) throw new IllegalArgumentException("pathSegment == null"); + push(pathSegment, 0, pathSegment.length(), false, false); + return this; + } + + public Builder addEncodedPathSegment(String encodedPathSegment) { + if (encodedPathSegment == null) { + throw new IllegalArgumentException("encodedPathSegment == null"); + } + push(encodedPathSegment, 0, encodedPathSegment.length(), false, true); + return this; + } + + public Builder setPathSegment(int index, String pathSegment) { + if (pathSegment == null) throw new IllegalArgumentException("pathSegment == null"); + String canonicalPathSegment = canonicalize( + pathSegment, 0, pathSegment.length(), PATH_SEGMENT_ENCODE_SET, false, false, true); + if (isDot(canonicalPathSegment) || isDotDot(canonicalPathSegment)) { + throw new IllegalArgumentException("unexpected path segment: " + pathSegment); + } + encodedPathSegments.set(index, canonicalPathSegment); + return this; + } + + public Builder setEncodedPathSegment(int index, String encodedPathSegment) { + if (encodedPathSegment == null) { + throw new IllegalArgumentException("encodedPathSegment == null"); + } + String canonicalPathSegment = canonicalize(encodedPathSegment, + 0, encodedPathSegment.length(), PATH_SEGMENT_ENCODE_SET, true, false, true); + encodedPathSegments.set(index, canonicalPathSegment); + if (isDot(canonicalPathSegment) || isDotDot(canonicalPathSegment)) { + throw new IllegalArgumentException("unexpected path segment: " + encodedPathSegment); + } + return this; + } + + public Builder removePathSegment(int index) { + encodedPathSegments.remove(index); + if (encodedPathSegments.isEmpty()) { + encodedPathSegments.add(""); // Always leave at least one '/'. + } + return this; + } + + public Builder encodedPath(String encodedPath) { + if (encodedPath == null) throw new IllegalArgumentException("encodedPath == null"); + if (!encodedPath.startsWith("/")) { + throw new IllegalArgumentException("unexpected encodedPath: " + encodedPath); + } + resolvePath(encodedPath, 0, encodedPath.length()); + return this; + } + + public Builder query(String query) { + this.encodedQueryNamesAndValues = query != null + ? queryStringToNamesAndValues(canonicalize(query, QUERY_ENCODE_SET, false, true, true)) + : null; + return this; + } + + public Builder encodedQuery(String encodedQuery) { + this.encodedQueryNamesAndValues = encodedQuery != null + ? queryStringToNamesAndValues( + canonicalize(encodedQuery, QUERY_ENCODE_SET, true, true, true)) + : null; + return this; + } + + /** Encodes the query parameter using UTF-8 and adds it to this URL's query string. */ + public Builder addQueryParameter(String name, String value) { + if (name == null) throw new IllegalArgumentException("name == null"); + if (encodedQueryNamesAndValues == null) encodedQueryNamesAndValues = new ArrayList<>(); + encodedQueryNamesAndValues.add( + canonicalize(name, QUERY_COMPONENT_ENCODE_SET, false, true, true)); + encodedQueryNamesAndValues.add(value != null + ? canonicalize(value, QUERY_COMPONENT_ENCODE_SET, false, true, true) + : null); + return this; + } + + /** Adds the pre-encoded query parameter to this URL's query string. */ + public Builder addEncodedQueryParameter(String encodedName, String encodedValue) { + if (encodedName == null) throw new IllegalArgumentException("encodedName == null"); + if (encodedQueryNamesAndValues == null) encodedQueryNamesAndValues = new ArrayList<>(); + encodedQueryNamesAndValues.add( + canonicalize(encodedName, QUERY_COMPONENT_ENCODE_SET, true, true, true)); + encodedQueryNamesAndValues.add(encodedValue != null + ? canonicalize(encodedValue, QUERY_COMPONENT_ENCODE_SET, true, true, true) + : null); + return this; + } + + public Builder setQueryParameter(String name, String value) { + removeAllQueryParameters(name); + addQueryParameter(name, value); + return this; + } + + public Builder setEncodedQueryParameter(String encodedName, String encodedValue) { + removeAllEncodedQueryParameters(encodedName); + addEncodedQueryParameter(encodedName, encodedValue); + return this; + } + + public Builder removeAllQueryParameters(String name) { + if (name == null) throw new IllegalArgumentException("name == null"); + if (encodedQueryNamesAndValues == null) return this; + String nameToRemove = canonicalize(name, QUERY_COMPONENT_ENCODE_SET, false, true, true); + removeAllCanonicalQueryParameters(nameToRemove); + return this; + } + + public Builder removeAllEncodedQueryParameters(String encodedName) { + if (encodedName == null) throw new IllegalArgumentException("encodedName == null"); + if (encodedQueryNamesAndValues == null) return this; + removeAllCanonicalQueryParameters( + canonicalize(encodedName, QUERY_COMPONENT_ENCODE_SET, true, true, true)); + return this; + } + + private void removeAllCanonicalQueryParameters(String canonicalName) { + for (int i = encodedQueryNamesAndValues.size() - 2; i >= 0; i -= 2) { + if (canonicalName.equals(encodedQueryNamesAndValues.get(i))) { + encodedQueryNamesAndValues.remove(i + 1); + encodedQueryNamesAndValues.remove(i); + if (encodedQueryNamesAndValues.isEmpty()) { + encodedQueryNamesAndValues = null; + return; + } + } + } + } + + public Builder fragment(String fragment) { + this.encodedFragment = fragment != null + ? canonicalize(fragment, FRAGMENT_ENCODE_SET, false, false, false) + : null; + return this; + } + + public Builder encodedFragment(String encodedFragment) { + this.encodedFragment = encodedFragment != null + ? canonicalize(encodedFragment, FRAGMENT_ENCODE_SET, true, false, false) + : null; + return this; + } + + /** + * Re-encodes the components of this URL so that it satisfies (obsolete) RFC 2396, which is + * particularly strict for certain components. + */ + Builder reencodeForUri() { + for (int i = 0, size = encodedPathSegments.size(); i < size; i++) { + String pathSegment = encodedPathSegments.get(i); + encodedPathSegments.set(i, + canonicalize(pathSegment, PATH_SEGMENT_ENCODE_SET_URI, true, false, true)); + } + if (encodedQueryNamesAndValues != null) { + for (int i = 0, size = encodedQueryNamesAndValues.size(); i < size; i++) { + String component = encodedQueryNamesAndValues.get(i); + if (component != null) { + encodedQueryNamesAndValues.set(i, + canonicalize(component, QUERY_COMPONENT_ENCODE_SET_URI, true, true, true)); + } + } + } + if (encodedFragment != null) { + encodedFragment = canonicalize( + encodedFragment, FRAGMENT_ENCODE_SET_URI, true, false, false); + } + return this; + } + + public HttpUrl build() { + if (scheme == null) throw new IllegalStateException("scheme == null"); + if (host == null) throw new IllegalStateException("host == null"); + return new HttpUrl(this); + } + + @Override public String toString() { + StringBuilder result = new StringBuilder(); + result.append(scheme); + result.append("://"); + + if (!encodedUsername.isEmpty() || !encodedPassword.isEmpty()) { + result.append(encodedUsername); + if (!encodedPassword.isEmpty()) { + result.append(':'); + result.append(encodedPassword); + } + result.append('@'); + } + + if (host.indexOf(':') != -1) { + // Host is an IPv6 address. + result.append('['); + result.append(host); + result.append(']'); + } else { + result.append(host); + } + + int effectivePort = effectivePort(); + if (effectivePort != defaultPort(scheme)) { + result.append(':'); + result.append(effectivePort); + } + + pathSegmentsToString(result, encodedPathSegments); + + if (encodedQueryNamesAndValues != null) { + result.append('?'); + namesAndValuesToQueryString(result, encodedQueryNamesAndValues); + } + + if (encodedFragment != null) { + result.append('#'); + result.append(encodedFragment); + } + + return result.toString(); + } + + enum ParseResult { + SUCCESS, + MISSING_SCHEME, + UNSUPPORTED_SCHEME, + INVALID_PORT, + INVALID_HOST, + } + + ParseResult parse(HttpUrl base, String input) { + int pos = skipLeadingAsciiWhitespace(input, 0, input.length()); + int limit = skipTrailingAsciiWhitespace(input, pos, input.length()); + + // Scheme. + int schemeDelimiterOffset = schemeDelimiterOffset(input, pos, limit); + if (schemeDelimiterOffset != -1) { + if (input.regionMatches(true, pos, "https:", 0, 6)) { + this.scheme = "https"; + pos += "https:".length(); + } else if (input.regionMatches(true, pos, "http:", 0, 5)) { + this.scheme = "http"; + pos += "http:".length(); + } else { + return ParseResult.UNSUPPORTED_SCHEME; // Not an HTTP scheme. + } + } else if (base != null) { + this.scheme = base.scheme; + } else { + return ParseResult.MISSING_SCHEME; // No scheme. + } + + // Authority. + boolean hasUsername = false; + boolean hasPassword = false; + int slashCount = slashCount(input, pos, limit); + if (slashCount >= 2 || base == null || !base.scheme.equals(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) { + int componentDelimiterOffset = delimiterOffset(input, pos, limit, "@/\\?#"); + int c = componentDelimiterOffset != limit + ? input.charAt(componentDelimiterOffset) + : -1; + switch (c) { + case '@': + // User info precedes. + if (!hasPassword) { + int passwordColonOffset = delimiterOffset( + input, pos, componentDelimiterOffset, ":"); + String canonicalUsername = canonicalize( + input, pos, passwordColonOffset, USERNAME_ENCODE_SET, true, false, true); + this.encodedUsername = hasUsername + ? this.encodedUsername + "%40" + canonicalUsername + : canonicalUsername; + if (passwordColonOffset != componentDelimiterOffset) { + hasPassword = true; + this.encodedPassword = canonicalize(input, passwordColonOffset + 1, + componentDelimiterOffset, PASSWORD_ENCODE_SET, true, false, true); + } + hasUsername = true; + } else { + this.encodedPassword = this.encodedPassword + "%40" + canonicalize( + input, pos, componentDelimiterOffset, PASSWORD_ENCODE_SET, true, false, true); + } + pos = componentDelimiterOffset + 1; + break; + + case -1: + case '/': + case '\\': + case '?': + case '#': + // Host info precedes. + int portColonOffset = portColonOffset(input, pos, componentDelimiterOffset); + if (portColonOffset + 1 < componentDelimiterOffset) { + this.host = canonicalizeHost(input, pos, portColonOffset); + this.port = parsePort(input, portColonOffset + 1, componentDelimiterOffset); + if (this.port == -1) return ParseResult.INVALID_PORT; // Invalid port. + } else { + this.host = canonicalizeHost(input, pos, portColonOffset); + this.port = defaultPort(this.scheme); + } + if (this.host == null) return ParseResult.INVALID_HOST; // Invalid host. + 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.charAt(pos) == '#') { + encodedQuery(base.encodedQuery()); + } + } + + // Resolve the relative path. + int pathDelimiterOffset = delimiterOffset(input, pos, limit, "?#"); + resolvePath(input, pos, pathDelimiterOffset); + pos = pathDelimiterOffset; + + // Query. + if (pos < limit && input.charAt(pos) == '?') { + int queryDelimiterOffset = delimiterOffset(input, pos, limit, "#"); + this.encodedQueryNamesAndValues = queryStringToNamesAndValues(canonicalize( + input, pos + 1, queryDelimiterOffset, QUERY_ENCODE_SET, true, true, true)); + pos = queryDelimiterOffset; + } + + // Fragment. + if (pos < limit && input.charAt(pos) == '#') { + this.encodedFragment = canonicalize( + input, pos + 1, limit, FRAGMENT_ENCODE_SET, true, false, false); + } + + return ParseResult.SUCCESS; + } + + private void resolvePath(String input, int pos, int limit) { + // Read a delimiter. + if (pos == limit) { + // Empty path: keep the base path as-is. + return; + } + char c = input.charAt(pos); + if (c == '/' || c == '\\') { + // Absolute path: reset to the default "/". + encodedPathSegments.clear(); + encodedPathSegments.add(""); + pos++; + } else { + // Relative path: clear everything after the last '/'. + encodedPathSegments.set(encodedPathSegments.size() - 1, ""); + } + + // Read path segments. + for (int i = pos; i < limit; ) { + int pathSegmentDelimiterOffset = delimiterOffset(input, i, limit, "/\\"); + boolean segmentHasTrailingSlash = pathSegmentDelimiterOffset < limit; + push(input, i, pathSegmentDelimiterOffset, segmentHasTrailingSlash, true); + i = pathSegmentDelimiterOffset; + if (segmentHasTrailingSlash) i++; + } + } + + /** Adds a path segment. If the input is ".." or equivalent, this pops a path segment. */ + private void push(String input, int pos, int limit, boolean addTrailingSlash, + boolean alreadyEncoded) { + String segment = canonicalize( + input, pos, limit, PATH_SEGMENT_ENCODE_SET, alreadyEncoded, false, true); + if (isDot(segment)) { + return; // Skip '.' path segments. + } + if (isDotDot(segment)) { + pop(); + return; + } + if (encodedPathSegments.get(encodedPathSegments.size() - 1).isEmpty()) { + encodedPathSegments.set(encodedPathSegments.size() - 1, segment); + } else { + encodedPathSegments.add(segment); + } + if (addTrailingSlash) { + encodedPathSegments.add(""); + } + } + + private boolean isDot(String input) { + return input.equals(".") || input.equalsIgnoreCase("%2e"); + } + + private boolean isDotDot(String input) { + return input.equals("..") + || input.equalsIgnoreCase("%2e.") + || input.equalsIgnoreCase(".%2e") + || input.equalsIgnoreCase("%2e%2e"); + } + + /** + * 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", ""]. + */ + private void pop() { + String removed = encodedPathSegments.remove(encodedPathSegments.size() - 1); + + // Make sure the path ends with a '/' by either adding an empty string or clearing a segment. + if (removed.isEmpty() && !encodedPathSegments.isEmpty()) { + encodedPathSegments.set(encodedPathSegments.size() - 1, ""); + } else { + encodedPathSegments.add(""); + } + } + + /** + * Increments {@code pos} until {@code input[pos]} is not ASCII whitespace. Stops at {@code + * limit}. + */ + private int skipLeadingAsciiWhitespace(String input, int pos, int limit) { + for (int i = pos; i < limit; i++) { + switch (input.charAt(i)) { + case '\t': + case '\n': + case '\f': + case '\r': + case ' ': + continue; + default: + return i; + } + } + return limit; + } + + /** + * Decrements {@code limit} until {@code input[limit - 1]} is not ASCII whitespace. Stops at + * {@code pos}. + */ + private int skipTrailingAsciiWhitespace(String input, int pos, int limit) { + for (int i = limit - 1; i >= pos; i--) { + switch (input.charAt(i)) { + case '\t': + case '\n': + case '\f': + case '\r': + case ' ': + continue; + default: + return i + 1; + } + } + return pos; + } + + /** + * Returns the index of the ':' in {@code input} that is after scheme characters. Returns -1 if + * {@code input} does not have a scheme that starts at {@code pos}. + */ + private static int schemeDelimiterOffset(String input, int pos, int limit) { + if (limit - pos < 2) return -1; + + char c0 = input.charAt(pos); + if ((c0 < 'a' || c0 > 'z') && (c0 < 'A' || c0 > 'Z')) return -1; // Not a scheme start char. + + for (int i = pos + 1; i < limit; i++) { + char c = input.charAt(i); + + if ((c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z') + || (c >= '0' && c <= '9') + || c == '+' + || c == '-' + || c == '.') { + continue; // Scheme character. Keep going. + } else if (c == ':') { + return i; // Scheme prefix! + } else { + return -1; // Non-scheme character before the first ':'. + } + } + + return -1; // No ':'; doesn't start with a scheme. + } + + /** Returns the number of '/' and '\' slashes in {@code input}, starting at {@code pos}. */ + private static int slashCount(String input, int pos, int limit) { + int slashCount = 0; + while (pos < limit) { + char c = input.charAt(pos); + if (c == '\\' || c == '/') { + slashCount++; + pos++; + } else { + break; + } + } + return slashCount; + } + + /** Finds the first ':' in {@code input}, skipping characters between square braces "[...]". */ + private static int portColonOffset(String input, int pos, int limit) { + for (int i = pos; i < limit; i++) { + switch (input.charAt(i)) { + case '[': + while (++i < limit) { + if (input.charAt(i) == ']') break; + } + break; + case ':': + return i; + } + } + return limit; // No colon. + } + + private static String canonicalizeHost(String input, int pos, int limit) { + // Start by percent decoding the host. The WHATWG spec suggests doing this only after we've + // checked for IPv6 square braces. But Chrome does it first, and that's more lenient. + String percentDecoded = percentDecode(input, pos, limit, false); + + // If the input is encased in square braces "[...]", drop 'em. We have an IPv6 address. + if (percentDecoded.startsWith("[") && percentDecoded.endsWith("]")) { + InetAddress inetAddress = decodeIpv6(percentDecoded, 1, percentDecoded.length() - 1); + if (inetAddress == null) return null; + byte[] address = inetAddress.getAddress(); + if (address.length == 16) return inet6AddressToAscii(address); + throw new AssertionError(); + } + + return domainToAscii(percentDecoded); + } + + /** Decodes an IPv6 address like 1111:2222:3333:4444:5555:6666:7777:8888 or ::1. */ + private static InetAddress decodeIpv6(String input, int pos, int limit) { + byte[] address = new byte[16]; + int b = 0; + int compress = -1; + int groupOffset = -1; + + for (int i = pos; i < limit; ) { + if (b == address.length) return null; // Too many groups. + + // Read a delimiter. + if (i + 2 <= limit && input.regionMatches(i, "::", 0, 2)) { + // Compression "::" delimiter, which is anywhere in the input, including its prefix. + if (compress != -1) return null; // Multiple "::" delimiters. + i += 2; + b += 2; + compress = b; + if (i == limit) break; + } else if (b != 0) { + // Group separator ":" delimiter. + if (input.regionMatches(i, ":", 0, 1)) { + i++; + } else if (input.regionMatches(i, ".", 0, 1)) { + // If we see a '.', rewind to the beginning of the previous group and parse as IPv4. + if (!decodeIpv4Suffix(input, groupOffset, limit, address, b - 2)) return null; + b += 2; // We rewound two bytes and then added four. + break; + } else { + return null; // Wrong delimiter. + } + } + + // Read a group, one to four hex digits. + int value = 0; + groupOffset = i; + for (; i < limit; i++) { + char c = input.charAt(i); + int hexDigit = decodeHexDigit(c); + if (hexDigit == -1) break; + value = (value << 4) + hexDigit; + } + int groupLength = i - groupOffset; + if (groupLength == 0 || groupLength > 4) return null; // Group is the wrong size. + + // We've successfully read a group. Assign its value to our byte array. + address[b++] = (byte) ((value >>> 8) & 0xff); + address[b++] = (byte) (value & 0xff); + } + + // All done. If compression happened, we need to move bytes to the right place in the + // address. Here's a sample: + // + // input: "1111:2222:3333::7777:8888" + // before: { 11, 11, 22, 22, 33, 33, 00, 00, 77, 77, 88, 88, 00, 00, 00, 00 } + // compress: 6 + // b: 10 + // after: { 11, 11, 22, 22, 33, 33, 00, 00, 00, 00, 00, 00, 77, 77, 88, 88 } + // + if (b != address.length) { + if (compress == -1) return null; // Address didn't have compression or enough groups. + System.arraycopy(address, compress, address, address.length - (b - compress), b - compress); + Arrays.fill(address, compress, compress + (address.length - b), (byte) 0); + } + + try { + return InetAddress.getByAddress(address); + } catch (UnknownHostException e) { + throw new AssertionError(); + } + } + + /** Decodes an IPv4 address suffix of an IPv6 address, like 1111::5555:6666:192.168.0.1. */ + private static boolean decodeIpv4Suffix( + String input, int pos, int limit, byte[] address, int addressOffset) { + int b = addressOffset; + + for (int i = pos; i < limit; ) { + if (b == address.length) return false; // Too many groups. + + // Read a delimiter. + if (b != addressOffset) { + if (input.charAt(i) != '.') return false; // Wrong delimiter. + i++; + } + + // Read 1 or more decimal digits for a value in 0..255. + int value = 0; + int groupOffset = i; + for (; i < limit; i++) { + char c = input.charAt(i); + if (c < '0' || c > '9') break; + if (value == 0 && groupOffset != i) return false; // Reject unnecessary leading '0's. + value = (value * 10) + c - '0'; + if (value > 255) return false; // Value out of range. + } + int groupLength = i - groupOffset; + if (groupLength == 0) return false; // No digits. + + // We've successfully read a byte. + address[b++] = (byte) value; + } + + if (b != addressOffset + 4) return false; // Too few groups. We wanted exactly four. + return true; // Success. + } + + /** + * Performs IDN ToASCII encoding and canonicalize the result to lowercase. e.g. This converts + * {@code ☃.net} to {@code xn--n3h.net}, and {@code WwW.GoOgLe.cOm} to {@code www.google.com}. + * {@code null} will be returned if the input cannot be ToASCII encoded or if the result + * contains unsupported ASCII characters. + */ + private static String domainToAscii(String input) { + try { + String result = IDN.toASCII(input).toLowerCase(Locale.US); + if (result.isEmpty()) return null; + + // Confirm that the IDN ToASCII result doesn't contain any illegal characters. + if (containsInvalidHostnameAsciiCodes(result)) { + return null; + } + // TODO: implement all label limits. + return result; + } catch (IllegalArgumentException e) { + return null; + } + } + + private static boolean containsInvalidHostnameAsciiCodes(String hostnameAscii) { + for (int i = 0; i < hostnameAscii.length(); i++) { + char c = hostnameAscii.charAt(i); + // The WHATWG Host parsing rules accepts some character codes which are invalid by + // definition for OkHttp's host header checks (and the WHATWG Host syntax definition). Here + // we rule out characters that would cause problems in host headers. + if (c <= '\u001f' || c >= '\u007f') { + return true; + } + // Check for the characters mentioned in the WHATWG Host parsing spec: + // U+0000, U+0009, U+000A, U+000D, U+0020, "#", "%", "/", ":", "?", "@", "[", "\", and "]" + // (excluding the characters covered above). + if (" #%/:?@[\\]".indexOf(c) != -1) { + return true; + } + } + return false; + } + + private static String inet6AddressToAscii(byte[] address) { + // Go through the address looking for the longest run of 0s. Each group is 2-bytes. + int longestRunOffset = -1; + int longestRunLength = 0; + for (int i = 0; i < address.length; i += 2) { + int currentRunOffset = i; + while (i < 16 && address[i] == 0 && address[i + 1] == 0) { + i += 2; + } + int currentRunLength = i - currentRunOffset; + if (currentRunLength > longestRunLength) { + longestRunOffset = currentRunOffset; + longestRunLength = currentRunLength; + } + } + + // Emit each 2-byte group in hex, separated by ':'. The longest run of zeroes is "::". + Buffer result = new Buffer(); + for (int i = 0; i < address.length; ) { + if (i == longestRunOffset) { + result.writeByte(':'); + i += longestRunLength; + if (i == 16) result.writeByte(':'); + } else { + if (i > 0) result.writeByte(':'); + int group = (address[i] & 0xff) << 8 | address[i + 1] & 0xff; + result.writeHexadecimalUnsignedLong(group); + i += 2; + } + } + return result.readUtf8(); + } + + private static int parsePort(String input, int pos, int limit) { + try { + // Canonicalize the port string to skip '\n' etc. + String portString = canonicalize(input, pos, limit, "", false, false, true); + int i = Integer.parseInt(portString); + if (i > 0 && i <= 65535) return i; + return -1; + } catch (NumberFormatException e) { + return -1; // Invalid port. + } + } + } + + /** + * Returns the index of the first character in {@code input} that contains a character in {@code + * delimiters}. Returns limit if there is no such character. + */ + private static int delimiterOffset(String input, int pos, int limit, String delimiters) { + for (int i = pos; i < limit; i++) { + if (delimiters.indexOf(input.charAt(i)) != -1) return i; + } + return limit; + } + + static String percentDecode(String encoded, boolean plusIsSpace) { + return percentDecode(encoded, 0, encoded.length(), plusIsSpace); + } + + private List percentDecode(List list, boolean plusIsSpace) { + List result = new ArrayList<>(list.size()); + for (String s : list) { + result.add(s != null ? percentDecode(s, plusIsSpace) : null); + } + return Collections.unmodifiableList(result); + } + + static String percentDecode(String encoded, int pos, int limit, boolean plusIsSpace) { + for (int i = pos; i < limit; i++) { + char c = encoded.charAt(i); + if (c == '%' || (c == '+' && plusIsSpace)) { + // Slow path: the character at i requires decoding! + Buffer out = new Buffer(); + out.writeUtf8(encoded, pos, i); + percentDecode(out, encoded, i, limit, plusIsSpace); + return out.readUtf8(); + } + } + + // Fast path: no characters in [pos..limit) required decoding. + return encoded.substring(pos, limit); + } + + static void percentDecode(Buffer out, String encoded, int pos, int limit, boolean plusIsSpace) { + int codePoint; + for (int i = pos; i < limit; i += Character.charCount(codePoint)) { + codePoint = encoded.codePointAt(i); + if (codePoint == '%' && i + 2 < limit) { + int d1 = decodeHexDigit(encoded.charAt(i + 1)); + int d2 = decodeHexDigit(encoded.charAt(i + 2)); + if (d1 != -1 && d2 != -1) { + out.writeByte((d1 << 4) + d2); + i += 2; + continue; + } + } else if (codePoint == '+' && plusIsSpace) { + out.writeByte(' '); + continue; + } + out.writeUtf8CodePoint(codePoint); + } + } + + static int decodeHexDigit(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; + } + + /** + * Returns a substring of {@code input} on the range {@code [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 {@code 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 plusIsSpace true to encode '+' as "%2B" if it is not already encoded. + * @param asciiOnly true to encode all non-ASCII codepoints. + */ + static String canonicalize(String input, int pos, int limit, String encodeSet, + boolean alreadyEncoded, boolean plusIsSpace, boolean asciiOnly) { + int codePoint; + for (int i = pos; i < limit; i += Character.charCount(codePoint)) { + codePoint = input.codePointAt(i); + if (codePoint < 0x20 + || codePoint == 0x7f + || (codePoint >= 0x80 && asciiOnly) + || encodeSet.indexOf(codePoint) != -1 + || (codePoint == '%' && !alreadyEncoded) + || (codePoint == '+' && plusIsSpace)) { + // Slow path: the character at i requires encoding! + Buffer out = new Buffer(); + out.writeUtf8(input, pos, i); + canonicalize(out, input, i, limit, encodeSet, alreadyEncoded, plusIsSpace, asciiOnly); + return out.readUtf8(); + } + } + + // Fast path: no characters in [pos..limit) required encoding. + return input.substring(pos, limit); + } + + static void canonicalize(Buffer out, String input, int pos, int limit, + String encodeSet, boolean alreadyEncoded, boolean plusIsSpace, boolean asciiOnly) { + Buffer utf8Buffer = null; // Lazily allocated. + int codePoint; + for (int i = pos; i < limit; i += Character.charCount(codePoint)) { + codePoint = input.codePointAt(i); + if (alreadyEncoded + && (codePoint == '\t' || codePoint == '\n' || codePoint == '\f' || codePoint == '\r')) { + // Skip this character. + } else if (codePoint == '+' && plusIsSpace) { + // Encode '+' as '%2B' since we permit ' ' to be encoded as either '+' or '%20'. + out.writeUtf8(alreadyEncoded ? "+" : "%2B"); + } else if (codePoint < 0x20 + || codePoint == 0x7f + || (codePoint >= 0x80 && asciiOnly) + || encodeSet.indexOf(codePoint) != -1 + || (codePoint == '%' && !alreadyEncoded)) { + // Percent encode this character. + if (utf8Buffer == null) { + utf8Buffer = new Buffer(); + } + utf8Buffer.writeUtf8CodePoint(codePoint); + while (!utf8Buffer.exhausted()) { + int b = utf8Buffer.readByte() & 0xff; + out.writeByte('%'); + out.writeByte(HEX_DIGITS[(b >> 4) & 0xf]); + out.writeByte(HEX_DIGITS[b & 0xf]); + } + } else { + // This character doesn't need encoding. Just copy it over. + out.writeUtf8CodePoint(codePoint); + } + } + } + + static String canonicalize(String input, String encodeSet, boolean alreadyEncoded, + boolean plusIsSpace, boolean asciiOnly) { + return canonicalize( + input, 0, input.length(), encodeSet, alreadyEncoded, plusIsSpace, asciiOnly); + } +} diff --git a/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/Request.java b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/Request.java new file mode 100644 index 00000000000..27319824112 --- /dev/null +++ b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/Request.java @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2013 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. + */ +/* + * Forked from OkHttp 2.7.0 + */ + +package io.grpc.okhttp.internal; + +import com.squareup.okhttp.internal.http.HttpMethod; +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.util.List; + +/** + * An HTTP request. Instances of this class are immutable if their {@link #body} + * is null or itself immutable. + */ +public final class Request { + private final HttpUrl url; + private final String method; + private final Headers headers; + private final RequestBody body; + private final Object tag; + + private volatile URL javaNetUrl; // Lazily initialized. + private volatile URI javaNetUri; // Lazily initialized. + private volatile CacheControl cacheControl; // Lazily initialized. + + private Request(Builder builder) { + this.url = builder.url; + this.method = builder.method; + this.headers = builder.headers.build(); + this.body = builder.body; + this.tag = builder.tag != null ? builder.tag : this; + } + + public HttpUrl httpUrl() { + return url; + } + + public URL url() { + URL result = javaNetUrl; + return result != null ? result : (javaNetUrl = url.url()); + } + + public URI uri() throws IOException { + try { + URI result = javaNetUri; + return result != null ? result : (javaNetUri = url.uri()); + } catch (IllegalStateException e) { + throw new IOException(e.getMessage()); + } + } + + public String urlString() { + return url.toString(); + } + + public String method() { + return method; + } + + public Headers headers() { + return headers; + } + + public String header(String name) { + return headers.get(name); + } + + public List headers(String name) { + return headers.values(name); + } + + public RequestBody body() { + return body; + } + + public Object tag() { + return tag; + } + + public Builder newBuilder() { + return new Builder(this); + } + + /** + * Returns the cache control directives for this response. This is never null, + * even if this response contains no {@code Cache-Control} header. + */ + public CacheControl cacheControl() { + CacheControl result = cacheControl; + return result != null ? result : (cacheControl = CacheControl.parse(headers)); + } + + public boolean isHttps() { + return url.isHttps(); + } + + @Override public String toString() { + return "Request{method=" + + method + + ", url=" + + url + + ", tag=" + + (tag != this ? tag : null) + + '}'; + } + + public static class Builder { + private HttpUrl url; + private String method; + private Headers.Builder headers; + private RequestBody body; + private Object tag; + + public Builder() { + this.method = "GET"; + this.headers = new Headers.Builder(); + } + + private Builder(Request request) { + this.url = request.url; + this.method = request.method; + this.body = request.body; + this.tag = request.tag; + this.headers = request.headers.newBuilder(); + } + + public Builder url(HttpUrl url) { + if (url == null) throw new IllegalArgumentException("url == null"); + this.url = url; + return this; + } + + /** + * Sets the URL target of this request. + * + * @throws IllegalArgumentException if {@code url} is not a valid HTTP or HTTPS URL. Avoid this + * exception by calling {@link HttpUrl#parse}; it returns null for invalid URLs. + */ + public Builder url(String url) { + if (url == null) throw new IllegalArgumentException("url == null"); + + // Silently replace websocket URLs with HTTP URLs. + if (url.regionMatches(true, 0, "ws:", 0, 3)) { + url = "http:" + url.substring(3); + } else if (url.regionMatches(true, 0, "wss:", 0, 4)) { + url = "https:" + url.substring(4); + } + + HttpUrl parsed = HttpUrl.parse(url); + if (parsed == null) throw new IllegalArgumentException("unexpected url: " + url); + return url(parsed); + } + + /** + * Sets the URL target of this request. + * + * @throws IllegalArgumentException if the scheme of {@code url} is not {@code http} or {@code + * https}. + */ + public Builder url(URL url) { + if (url == null) throw new IllegalArgumentException("url == null"); + HttpUrl parsed = HttpUrl.get(url); + if (parsed == null) throw new IllegalArgumentException("unexpected url: " + url); + return url(parsed); + } + + /** + * Sets the header named {@code name} to {@code value}. If this request + * already has any headers with that name, they are all replaced. + */ + public Builder header(String name, String value) { + headers.set(name, value); + return this; + } + + /** + * Adds a header with {@code name} and {@code value}. Prefer this method for + * multiply-valued headers like "Cookie". + * + *

Note that for some headers including {@code Content-Length} and {@code Content-Encoding}, + * OkHttp may replace {@code value} with a header derived from the request body. + */ + public Builder addHeader(String name, String value) { + headers.add(name, value); + return this; + } + + public Builder removeHeader(String name) { + headers.removeAll(name); + return this; + } + + /** Removes all headers on this builder and adds {@code headers}. */ + public Builder headers(Headers headers) { + this.headers = headers.newBuilder(); + return this; + } + + /** + * Sets this request's {@code Cache-Control} header, replacing any cache + * control headers already present. If {@code cacheControl} doesn't define + * any directives, this clears this request's cache-control headers. + */ + public Builder cacheControl(CacheControl cacheControl) { + String value = cacheControl.toString(); + if (value.isEmpty()) return removeHeader("Cache-Control"); + return header("Cache-Control", value); + } + + public Builder get() { + return method("GET", null); + } + + public Builder head() { + return method("HEAD", null); + } + + public Builder post(RequestBody body) { + return method("POST", body); + } + + public Builder delete(RequestBody body) { + return method("DELETE", body); + } + + public Builder delete() { + return delete(RequestBody.create(null, new byte[0])); + } + + public Builder put(RequestBody body) { + return method("PUT", body); + } + + public Builder patch(RequestBody body) { + return method("PATCH", body); + } + + public Builder method(String method, RequestBody body) { + if (method == null || method.length() == 0) { + throw new IllegalArgumentException("method == null || method.length() == 0"); + } + if (body != null && !HttpMethod.permitsRequestBody(method)) { + throw new IllegalArgumentException("method " + method + " must not have a request body."); + } + if (body == null && HttpMethod.requiresRequestBody(method)) { + throw new IllegalArgumentException("method " + method + " must have a request body."); + } + this.method = method; + this.body = body; + return this; + } + + /** + * Attaches {@code tag} to the request. It can be used later to cancel the + * request. If the tag is unspecified or null, the request is canceled by + * using the request itself as the tag. + */ + public Builder tag(Object tag) { + this.tag = tag; + return this; + } + + public Request build() { + if (url == null) throw new IllegalStateException("url == null"); + return new Request(this); + } + } +} diff --git a/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/StatusLine.java b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/StatusLine.java new file mode 100644 index 00000000000..c4a196d4361 --- /dev/null +++ b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/StatusLine.java @@ -0,0 +1,94 @@ +/* + * Forked from OkHttp 2.7.0 + */ + +package io.grpc.okhttp.internal; + +import com.squareup.okhttp.Protocol; +import com.squareup.okhttp.Response; +import java.io.IOException; +import java.net.ProtocolException; + +/** An HTTP response status line like "HTTP/1.1 200 OK". */ +public final class StatusLine { + /** Numeric status code, 307: Temporary Redirect. */ + public static final int HTTP_TEMP_REDIRECT = 307; + public static final int HTTP_PERM_REDIRECT = 308; + public static final int HTTP_CONTINUE = 100; + + public final Protocol protocol; + public final int code; + public final String message; + + public StatusLine(Protocol protocol, int code, String message) { + this.protocol = protocol; + this.code = code; + this.message = message; + } + + public static StatusLine get(Response response) { + return new StatusLine(response.protocol(), response.code(), response.message()); + } + + public static StatusLine parse(String statusLine) throws IOException { + // H T T P / 1 . 1 2 0 0 T e m p o r a r y R e d i r e c t + // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 + + // Parse protocol like "HTTP/1.1" followed by a space. + int codeStart; + Protocol protocol; + if (statusLine.startsWith("HTTP/1.")) { + if (statusLine.length() < 9 || statusLine.charAt(8) != ' ') { + throw new ProtocolException("Unexpected status line: " + statusLine); + } + int httpMinorVersion = statusLine.charAt(7) - '0'; + codeStart = 9; + if (httpMinorVersion == 0) { + protocol = Protocol.HTTP_1_0; + } else if (httpMinorVersion == 1) { + protocol = Protocol.HTTP_1_1; + } else { + throw new ProtocolException("Unexpected status line: " + statusLine); + } + } else if (statusLine.startsWith("ICY ")) { + // Shoutcast uses ICY instead of "HTTP/1.0". + protocol = Protocol.HTTP_1_0; + codeStart = 4; + } else { + throw new ProtocolException("Unexpected status line: " + statusLine); + } + + // Parse response code like "200". Always 3 digits. + if (statusLine.length() < codeStart + 3) { + throw new ProtocolException("Unexpected status line: " + statusLine); + } + int code; + try { + code = Integer.parseInt(statusLine.substring(codeStart, codeStart + 3)); + } catch (NumberFormatException e) { + throw new ProtocolException("Unexpected status line: " + statusLine); + } + + // Parse an optional response message like "OK" or "Not Modified". If it + // exists, it is separated from the response code by a space. + String message = ""; + if (statusLine.length() > codeStart + 3) { + if (statusLine.charAt(codeStart + 3) != ' ') { + throw new ProtocolException("Unexpected status line: " + statusLine); + } + message = statusLine.substring(codeStart + 4); + } + + return new StatusLine(protocol, code, message); + } + + @Override public String toString() { + StringBuilder result = new StringBuilder(); + result.append(protocol == Protocol.HTTP_1_0 ? "HTTP/1.0" : "HTTP/1.1"); + result.append(' ').append(code); + if (message != null) { + result.append(' ').append(message); + } + return result.toString(); + } +} From 634e8e66c34796bf3e83df761db1945f4d48c888 Mon Sep 17 00:00:00 2001 From: beatrausch Date: Sat, 5 Mar 2022 20:26:37 +0100 Subject: [PATCH 2/4] okhttp: forked missing file to make okhttp dep compile only --- .../io/grpc/okhttp/internal/Credentials.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/Credentials.java diff --git a/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/Credentials.java b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/Credentials.java new file mode 100644 index 00000000000..08a46ada7a7 --- /dev/null +++ b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/Credentials.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2014 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. + */ +/* + * Forked from OkHttp 2.7.0 + */ +package io.grpc.okhttp.internal; + +import java.io.UnsupportedEncodingException; +import okio.ByteString; + +/** Factory for HTTP authorization credentials. */ +public final class Credentials { + private Credentials() { + } + + /** Returns an auth credential for the Basic scheme. */ + public static String basic(String userName, String password) { + try { + String usernameAndPassword = userName + ":" + password; + byte[] bytes = usernameAndPassword.getBytes("ISO-8859-1"); + String encoded = ByteString.of(bytes).base64(); + return "Basic " + encoded; + } catch (UnsupportedEncodingException e) { + throw new AssertionError(); + } + } +} From 81404d95450df353300d863ce63657d7bc5cf2dc Mon Sep 17 00:00:00 2001 From: beatrausch Date: Sat, 5 Mar 2022 20:21:19 +0100 Subject: [PATCH 3/4] okhttp: moved url and request files to proxy packge --- .../java/io/grpc/okhttp/internal/{ => proxy}/HttpUrl.java | 2 +- .../java/io/grpc/okhttp/internal/{ => proxy}/Request.java | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) rename okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/{ => proxy}/HttpUrl.java (99%) rename okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/{ => proxy}/Request.java (99%) diff --git a/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/HttpUrl.java b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/proxy/HttpUrl.java similarity index 99% rename from okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/HttpUrl.java rename to okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/proxy/HttpUrl.java index b0ed5c80438..248bd7a05bb 100644 --- a/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/HttpUrl.java +++ b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/proxy/HttpUrl.java @@ -17,7 +17,7 @@ * Forked from OkHttp 2.7.0 */ -package io.grpc.okhttp.internal; +package io.grpc.okhttp.internal.proxy; import java.net.IDN; import java.net.InetAddress; diff --git a/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/Request.java b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/proxy/Request.java similarity index 99% rename from okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/Request.java rename to okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/proxy/Request.java index 27319824112..ce6bd18113e 100644 --- a/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/Request.java +++ b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/proxy/Request.java @@ -17,9 +17,11 @@ * Forked from OkHttp 2.7.0 */ -package io.grpc.okhttp.internal; +package io.grpc.okhttp.internal.proxy; import com.squareup.okhttp.internal.http.HttpMethod; +import io.grpc.okhttp.internal.Headers; + import java.io.IOException; import java.net.URI; import java.net.URL; From 5baae53507139b80eb9ca2cba16d59b33bc8a0e2 Mon Sep 17 00:00:00 2001 From: beatrausch Date: Sat, 5 Mar 2022 21:04:17 +0100 Subject: [PATCH 4/4] okhttp: removed unused methods from forked files; fixed build --- interop-testing/build.gradle | 3 +- okhttp/build.gradle | 8 +- .../io/grpc/okhttp/OkHttpClientTransport.java | 11 +- .../java/io/grpc/okhttp/internal/Headers.java | 168 +-- .../io/grpc/okhttp/internal/StatusLine.java | 7 - .../grpc/okhttp/internal/proxy/HttpUrl.java | 1166 +---------------- .../grpc/okhttp/internal/proxy/Request.java | 219 +--- 7 files changed, 31 insertions(+), 1551 deletions(-) diff --git a/interop-testing/build.gradle b/interop-testing/build.gradle index 5a13c616143..cc66b4463a4 100644 --- a/interop-testing/build.gradle +++ b/interop-testing/build.gradle @@ -46,7 +46,8 @@ dependencies { testImplementation project(':grpc-context').sourceSets.test.output, project(':grpc-api').sourceSets.test.output, project(':grpc-core').sourceSets.test.output, - libraries.mockito + libraries.mockito, + libraries.okhttp alpnagent libraries.jetty_alpn_agent } diff --git a/okhttp/build.gradle b/okhttp/build.gradle index 999f21e7c10..490e527cbf1 100644 --- a/okhttp/build.gradle +++ b/okhttp/build.gradle @@ -11,16 +11,18 @@ description = "gRPC: OkHttp" evaluationDependsOn(project(':grpc-core').path) dependencies { - api project(':grpc-core'), - libraries.okhttp + api project(':grpc-core') implementation libraries.okio, libraries.guava, libraries.perfmark + // Make okhttp dependencies compile only + compileOnly libraries.okhttp // Tests depend on base class defined by core module. testImplementation project(':grpc-core').sourceSets.test.output, project(':grpc-api').sourceSets.test.output, project(':grpc-testing'), - project(':grpc-netty') + project(':grpc-netty'), + libraries.okhttp signature "org.codehaus.mojo.signature:java17:1.0@signature" signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature" } diff --git a/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java b/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java index cf7cae19d8a..e233fa20028 100644 --- a/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java +++ b/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java @@ -28,10 +28,6 @@ import com.google.common.base.Supplier; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; -import com.squareup.okhttp.Credentials; -import com.squareup.okhttp.HttpUrl; -import com.squareup.okhttp.Request; -import com.squareup.okhttp.internal.http.StatusLine; import io.grpc.Attributes; import io.grpc.CallOptions; import io.grpc.ClientStreamTracer; @@ -61,6 +57,8 @@ import io.grpc.internal.TransportTracer; import io.grpc.okhttp.ExceptionHandlingFrameWriter.TransportExceptionHandler; import io.grpc.okhttp.internal.ConnectionSpec; +import io.grpc.okhttp.internal.Credentials; +import io.grpc.okhttp.internal.StatusLine; import io.grpc.okhttp.internal.framed.ErrorCode; import io.grpc.okhttp.internal.framed.FrameReader; import io.grpc.okhttp.internal.framed.FrameWriter; @@ -69,6 +67,8 @@ import io.grpc.okhttp.internal.framed.Http2; import io.grpc.okhttp.internal.framed.Settings; import io.grpc.okhttp.internal.framed.Variant; +import io.grpc.okhttp.internal.proxy.HttpUrl; +import io.grpc.okhttp.internal.proxy.Request; import io.perfmark.PerfMark; import java.io.EOFException; import java.io.IOException; @@ -709,12 +709,13 @@ private Socket createHttpProxySocket(InetSocketAddress address, InetSocketAddres } private Request createHttpProxyRequest(InetSocketAddress address, String proxyUsername, - String proxyPassword) { + String proxyPassword) { HttpUrl tunnelUrl = new HttpUrl.Builder() .scheme("https") .host(address.getHostName()) .port(address.getPort()) .build(); + Request.Builder request = new Request.Builder() .url(tunnelUrl) .header("Host", tunnelUrl.host() + ":" + tunnelUrl.port()) diff --git a/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/Headers.java b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/Headers.java index a81e67472ae..4723a87dfbe 100644 --- a/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/Headers.java +++ b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/Headers.java @@ -15,32 +15,16 @@ * limitations under the License. */ /* - * Forked from OkHttp 2.7.0 + * Forked from OkHttp 2.7.0 com.squareup.okhttp.Headers */ - package io.grpc.okhttp.internal; -import com.squareup.okhttp.internal.http.HttpDate; import java.util.ArrayList; import java.util.Collections; -import java.util.Date; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeSet; /** * The header fields of a single HTTP message. Values are uninterpreted strings; - * use {@code Request} and {@code Response} for interpreted headers. This class - * maintains the order of the header fields within the HTTP message. - * - *

This class tracks header values line-by-line. A field with multiple comma- - * separated values on the same line will be treated as a field with a single - * value by this class. It is the caller's responsibility to detect and split - * on commas if their field permits multiple values. This simplifies use of - * single-valued fields whose values routinely contain commas, such as cookies - * or dates. * *

This class trims whitespace from values. It never returns values with * leading or trailing whitespace. @@ -55,25 +39,11 @@ private Headers(Builder builder) { this.namesAndValues = builder.namesAndValues.toArray(new String[builder.namesAndValues.size()]); } - private Headers(String[] namesAndValues) { - this.namesAndValues = namesAndValues; - } - /** Returns the last value corresponding to the specified field, or null. */ public String get(String name) { return get(namesAndValues, name); } - /** - * Returns the last value corresponding to the specified field parsed as an - * HTTP date, or null if either the field is absent or cannot be parsed as a - * date. - */ - public Date getDate(String name) { - String value = get(name); - return value != null ? HttpDate.parse(value) : null; - } - /** Returns the number of field values. */ public int size() { return namesAndValues.length / 2; @@ -97,29 +67,6 @@ public String value(int index) { return namesAndValues[valueIndex]; } - /** Returns an immutable case-insensitive set of header names. */ - public Set names() { - TreeSet result = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); - for (int i = 0, size = size(); i < size; i++) { - result.add(name(i)); - } - return Collections.unmodifiableSet(result); - } - - /** Returns an immutable list of the header values for {@code name}. */ - public List values(String name) { - List result = null; - for (int i = 0, size = size(); i < size; i++) { - if (name.equalsIgnoreCase(name(i))) { - if (result == null) result = new ArrayList<>(2); - result.add(value(i)); - } - } - return result != null - ? Collections.unmodifiableList(result) - : Collections.emptyList(); - } - public Builder newBuilder() { Builder result = new Builder(); Collections.addAll(result.namesAndValues, namesAndValues); @@ -134,20 +81,6 @@ public Builder newBuilder() { return result.toString(); } - public Map> toMultimap() { - Map> result = new LinkedHashMap>(); - for (int i = 0, size = size(); i < size; i++) { - String name = name(i); - List values = result.get(name); - if (values == null) { - values = new ArrayList<>(2); - result.put(name, values); - } - values.add(value(i)); - } - return result; - } - private static String get(String[] namesAndValues, String name) { for (int i = namesAndValues.length - 2; i >= 0; i -= 2) { if (name.equalsIgnoreCase(namesAndValues[i])) { @@ -157,98 +90,9 @@ private static String get(String[] namesAndValues, String name) { return null; } - /** - * Returns headers for the alternating header names and values. There must be - * an even number of arguments, and they must alternate between header names - * and values. - */ - public static Headers of(String... namesAndValues) { - if (namesAndValues == null || namesAndValues.length % 2 != 0) { - throw new IllegalArgumentException("Expected alternating header names and values"); - } - - // Make a defensive copy and clean it up. - namesAndValues = namesAndValues.clone(); - for (int i = 0; i < namesAndValues.length; i++) { - if (namesAndValues[i] == null) throw new IllegalArgumentException("Headers cannot be null"); - namesAndValues[i] = namesAndValues[i].trim(); - } - - // Check for malformed headers. - for (int i = 0; i < namesAndValues.length; i += 2) { - String name = namesAndValues[i]; - String value = namesAndValues[i + 1]; - if (name.length() == 0 || name.indexOf('\0') != -1 || value.indexOf('\0') != -1) { - throw new IllegalArgumentException("Unexpected header: " + name + ": " + value); - } - } - - return new Headers(namesAndValues); - } - - /** - * Returns headers for the header names and values in the {@link Map}. - */ - public static Headers of(Map headers) { - if (headers == null) { - throw new IllegalArgumentException("Expected map with header names and values"); - } - - // Make a defensive copy and clean it up. - String[] namesAndValues = new String[headers.size() * 2]; - int i = 0; - for (Map.Entry header : headers.entrySet()) { - if (header.getKey() == null || header.getValue() == null) { - throw new IllegalArgumentException("Headers cannot be null"); - } - String name = header.getKey().trim(); - String value = header.getValue().trim(); - if (name.length() == 0 || name.indexOf('\0') != -1 || value.indexOf('\0') != -1) { - throw new IllegalArgumentException("Unexpected header: " + name + ": " + value); - } - namesAndValues[i] = name; - namesAndValues[i + 1] = value; - i += 2; - } - - return new Headers(namesAndValues); - } - public static final class Builder { private final List namesAndValues = new ArrayList<>(20); - /** - * Add a header line without any validation. Only appropriate for headers from the remote peer - * or cache. - */ - Builder addLenient(String line) { - int index = line.indexOf(":", 1); - if (index != -1) { - return addLenient(line.substring(0, index), line.substring(index + 1)); - } else if (line.startsWith(":")) { - // Work around empty header names and header names that start with a - // colon (created by old broken SPDY versions of the response cache). - return addLenient("", line.substring(1)); // Empty header name. - } else { - return addLenient("", line); // No header name. - } - } - - /** Add an header line containing a field name, a literal colon, and a value. */ - public Builder add(String line) { - int index = line.indexOf(":"); - if (index == -1) { - throw new IllegalArgumentException("Unexpected header: " + line); - } - return add(line.substring(0, index).trim(), line.substring(index + 1)); - } - - /** Add a field with the specified value. */ - public Builder add(String name, String value) { - checkNameAndValue(name, value); - return addLenient(name, value); - } - /** * Add a field with the specified value without any validation. Only * appropriate for headers from the remote peer or cache. @@ -301,16 +145,6 @@ private void checkNameAndValue(String name, String value) { } } - /** Equivalent to {@code build().get(name)}, but potentially faster. */ - public String get(String name) { - for (int i = namesAndValues.size() - 2; i >= 0; i -= 2) { - if (name.equalsIgnoreCase(namesAndValues.get(i))) { - return namesAndValues.get(i + 1); - } - } - return null; - } - public Headers build() { return new Headers(this); } diff --git a/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/StatusLine.java b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/StatusLine.java index c4a196d4361..ab72ee2d294 100644 --- a/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/StatusLine.java +++ b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/StatusLine.java @@ -1,11 +1,8 @@ /* * Forked from OkHttp 2.7.0 */ - package io.grpc.okhttp.internal; -import com.squareup.okhttp.Protocol; -import com.squareup.okhttp.Response; import java.io.IOException; import java.net.ProtocolException; @@ -26,10 +23,6 @@ public StatusLine(Protocol protocol, int code, String message) { this.message = message; } - public static StatusLine get(Response response) { - return new StatusLine(response.protocol(), response.code(), response.message()); - } - public static StatusLine parse(String statusLine) throws IOException { // H T T P / 1 . 1 2 0 0 T e m p o r a r y R e d i r e c t // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 diff --git a/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/proxy/HttpUrl.java b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/proxy/HttpUrl.java index 248bd7a05bb..31003c02ca1 100644 --- a/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/proxy/HttpUrl.java +++ b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/proxy/HttpUrl.java @@ -14,339 +14,43 @@ * limitations under the License. */ /* - * Forked from OkHttp 2.7.0 + * Forked from OkHttp 2.7.0 com.squareup.okhttp.HttpUrl */ - package io.grpc.okhttp.internal.proxy; import java.net.IDN; import java.net.InetAddress; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; import java.net.UnknownHostException; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.List; import java.util.Locale; -import java.util.Set; import okio.Buffer; /** - * A uniform resource locator (URL) with a scheme of either {@code http} or {@code https}. Use this - * class to compose and decompose Internet addresses. For example, this code will compose and print - * a URL for Google search:

   {@code
- *
- *   HttpUrl url = new HttpUrl.Builder()
- *       .scheme("https")
- *       .host("www.google.com")
- *       .addPathSegment("search")
- *       .addQueryParameter("q", "polar bears")
- *       .build();
- *   System.out.println(url);
- * }
- * - * which prints:
   {@code
- *
- *     https://www.google.com/search?q=polar%20bears
- * }
- * - * As another example, this code prints the human-readable query parameters of a Twitter search: - *
   {@code
- *
- *   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:
   {@code
- *
- *   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:
   {@code
- *
- *   HttpUrl base = HttpUrl.parse("https://www.youtube.com/user/WatchTheDaily/videos");
- *   HttpUrl link = base.resolve("../../watch?v=cbP2N1BQdYc");
- *   System.out.println(link);
- * }
- * - * which prints:
   {@code
- *
- *   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 ({@code mailto}, {@code file}, {@code - * ftp}), this class only supports {@code http} and {@code https}. Use {@link URI java.net.URI} for - * URLs with arbitrary schemes. - * - *

Username and Password

- * Username and password are either present, or the empty string {@code ""} 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 - * {@code square.com} or {@code localhost}, an IPv4 address like {@code 192.168.0.1}, or an IPv6 - * address like {@code ::1}. - * - *

Usually a webserver is reachable with multiple identifiers: its IP addresses, registered - * domain names, and even {@code localhost} when connecting from the server itself. Each of a - * webserver's names is a distinct URL and they are not interchangeable. For example, even if - * {@code http://square.github.io/dagger} and {@code 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 webserver. 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". Each path segment is prefixed with "/". This class offers methods - * to compose and decompose paths by segment. 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 "/", which is a path whose only segment is the 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 {@code cute #puppies} is encoded as {@code cute%20%23puppies} when used as a query - * parameter value. - * - *

Percent encoding

- * Percent encoding replaces a character (like {@code \ud83c\udf69}) with its UTF-8 hex bytes (like - * {@code %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 {@code ?} characters, otherwise it could be interpreted as the - * start of the URL's query. But within the query and fragment components, the {@code ?} character - * doesn't delimit anything and doesn't need to be escaped.

   {@code
- *
- *   HttpUrl url = HttpUrl.parse("http://who-let-the-dogs.out").newBuilder()
- *       .addPathSegment("_Who?_")
- *       .query("_Who?_")
- *       .fragment("_Who?_")
- *       .build();
- *   System.out.println(url);
- * }
- * - * This prints:
   {@code
- *
- *   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 transforms names to avoid - * confusing characters. This includes basic case folding: transforming shouting {@code SQUARE.COM} - * into cool and casual {@code square.com}. It also handles more exotic characters. For example, the - * Unicode trademark sign (™) could be confused for the letters "TM" in {@code http://ho™mail.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 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 {@link - * InetAddress} to establish connections. - * - *

Why another URL model?

- * Java includes both {@link URL java.net.URL} and {@link URI java.net.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, {@code java.net.URL} considers the following two URLs - * equal, and the {@link Object#equals equals()} method between them returns true: - *
    - *
  • http://square.github.io/ - *
  • http://google.github.io/ - *
- * This is because those two hosts share the same IP address. This is an old, bad design decision - * that makes {@code java.net.URL} unusable for many things. It shouldn't be used as a {@link - * java.util.Map Map} key or in a {@link 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 {@code java.net.URI} disagrees: - *
    - *
  • http://host:80/ - *
  • http://host - *
- * Both the unnecessary port specification ({@code :80}) and the absent trailing slash ({@code /}) - * 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.

   {@code
- *
- *   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).path());
- * }
- * - * By canonicalizing the input paths, they are complicit in directory traversal attacks. Code that - * checks only the path prefix may suffer! - *
   {@code
- *
- *    /static/images/../../../../../etc/passwd
- *    /static/images/../../../../../etc/passwd
- *    /etc/passwd
- * }
- * - *

If it works on the web, it should work in your application

- * The {@code 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. {@code 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 {@code 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 {@link HttpUrl} are well-formed and always have a scheme, host, and path. With - * {@code java.net.URL} it's possible to create an awkward URL like {@code 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: {@link #parse parse()} - * returns null if the input is an invalid URL. You can even be explicit about whether each - * component has been encoded already. + * Helper class to build a proxy URL. */ public final class HttpUrl { private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; - static final String USERNAME_ENCODE_SET = " \"':;<=>@[]^`{}|/\\?#"; - static final String PASSWORD_ENCODE_SET = " \"':;<=>@[]^`{}|/\\?#"; - static final String PATH_SEGMENT_ENCODE_SET = " \"<>^`{}|/\\?#"; - static final String PATH_SEGMENT_ENCODE_SET_URI = "[]"; - static final String QUERY_ENCODE_SET = " \"'<>#"; - static final String QUERY_COMPONENT_ENCODE_SET = " \"'<>#&="; - static final String QUERY_COMPONENT_ENCODE_SET_URI = "\\^`{|}"; - static final String FORM_ENCODE_SET = " \"':;<=>@[]^`{}|/\\?#&!$(),~"; - static final String FRAGMENT_ENCODE_SET = ""; - static final String FRAGMENT_ENCODE_SET_URI = " \"#<>\\^`{|}"; /** Either "http" or "https". */ private final String scheme; - /** Decoded username. */ - private final String username; - - /** Decoded password. */ - private final String password; - /** Canonical hostname. */ private final String host; /** Either 80, 443 or a user-specified port. In range [1..65535]. */ private final int port; - /** - * A list of canonical path segments. This list always contains at least one element, which may - * be the empty string. Each segment is formatted with a leading '/', so if path segments were - * ["a", "b", ""], then the encoded path would be "/a/b/". - */ - private final List pathSegments; - - /** - * Alternating, decoded query names and values, or null for no query. Names may be empty or - * non-empty, but never null. Values are null if the name has no corresponding '=' separator, or - * empty, or non-empty. - */ - private final List queryNamesAndValues; - - /** Decoded fragment. */ - private final String fragment; - /** Canonical URL. */ private final String url; private HttpUrl(Builder builder) { this.scheme = builder.scheme; - this.username = percentDecode(builder.encodedUsername, false); - this.password = percentDecode(builder.encodedPassword, false); this.host = builder.host; this.port = builder.effectivePort(); - this.pathSegments = percentDecode(builder.encodedPathSegments, false); - this.queryNamesAndValues = builder.encodedQueryNamesAndValues != null - ? percentDecode(builder.encodedQueryNamesAndValues, true) - : null; - this.fragment = builder.encodedFragment != null - ? percentDecode(builder.encodedFragment, false) - : null; this.url = builder.toString(); } - /** Returns this URL as a {@link URL java.net.URL}. */ - public URL url() { - try { - return new URL(url); - } catch (MalformedURLException e) { - throw new RuntimeException(e); // Unexpected! - } - } - - /** - * Returns this URL as a {@link URI java.net.URI}. Because {@code URI} forbids certain characters - * like {@code [} and {@code |}, the returned URI may escape more characters than this URL. - * - *

This method throws an unchecked {@link IllegalStateException} if it cannot be converted to a - * URI even after escaping forbidden characters. In particular, URLs that contain malformed - * percent escapes like {@code http://host/%xx} will trigger this exception. - */ - public URI uri() { - try { - String uri = newBuilder().reencodeForUri().toString(); - return new URI(uri); - } catch (URISyntaxException e) { - throw new IllegalStateException("not valid as a java.net.URI: " + url); - } - } - /** Returns either "http" or "https". */ public String scheme() { return scheme; @@ -356,31 +60,6 @@ public boolean isHttps() { return scheme.equals("https"); } - /** Returns the username, or an empty string if none is set. */ - public String encodedUsername() { - if (username.isEmpty()) return ""; - int usernameStart = scheme.length() + 3; // "://".length() == 3. - int usernameEnd = delimiterOffset(url, usernameStart, url.length(), ":@"); - return url.substring(usernameStart, usernameEnd); - } - - public String username() { - return username; - } - - /** Returns the password, or an empty string if none is set. */ - public String encodedPassword() { - if (password.isEmpty()) return ""; - int passwordStart = url.indexOf(':', scheme.length() + 3) + 1; - int passwordEnd = url.indexOf('@'); - return url.substring(passwordStart, passwordEnd); - } - - /** Returns the decoded password, or an empty string if none is present. */ - public String password() { - return password; - } - /** * Returns the host address suitable for use with {@link InetAddress#getAllByName(String)}. May * be: @@ -418,224 +97,15 @@ public static int defaultPort(String scheme) { } } - public int pathSize() { - return pathSegments.size(); - } - - /** - * Returns the entire path of this URL, encoded for use in HTTP resource resolution. The - * returned path is always nonempty and is prefixed with {@code /}. - */ - public String encodedPath() { - int pathStart = url.indexOf('/', scheme.length() + 3); // "://".length() == 3. - int pathEnd = delimiterOffset(url, pathStart, url.length(), "?#"); - return url.substring(pathStart, pathEnd); - } - - static void pathSegmentsToString(StringBuilder out, List pathSegments) { - for (int i = 0, size = pathSegments.size(); i < size; i++) { - out.append('/'); - out.append(pathSegments.get(i)); - } - } - - public List encodedPathSegments() { - int pathStart = url.indexOf('/', scheme.length() + 3); - int pathEnd = delimiterOffset(url, pathStart, url.length(), "?#"); - List result = new ArrayList<>(); - for (int i = pathStart; i < pathEnd; ) { - i++; // Skip the '/'. - int segmentEnd = delimiterOffset(url, i, pathEnd, "/"); - result.add(url.substring(i, segmentEnd)); - i = segmentEnd; - } - return result; - } - - public List pathSegments() { - return pathSegments; - } - - /** - * Returns the query of this URL, encoded for use in HTTP resource resolution. The returned string - * may be null (for URLs with no query), empty (for URLs with an empty query) or non-empty (all - * other URLs). - */ - public String encodedQuery() { - if (queryNamesAndValues == null) return null; // No query. - int queryStart = url.indexOf('?') + 1; - int queryEnd = delimiterOffset(url, queryStart + 1, url.length(), "#"); - return url.substring(queryStart, queryEnd); - } - - static void namesAndValuesToQueryString(StringBuilder out, List namesAndValues) { - for (int i = 0, size = namesAndValues.size(); i < size; i += 2) { - String name = namesAndValues.get(i); - String value = namesAndValues.get(i + 1); - if (i > 0) out.append('&'); - out.append(name); - if (value != null) { - out.append('='); - out.append(value); - } - } - } - - /** - * Cuts {@code encodedQuery} up into alternating parameter names and values. This divides a - * query string like {@code subject=math&easy&problem=5-2=3} into the list {@code ["subject", - * "math", "easy", null, "problem", "5-2=3"]}. Note that values may be null and may contain - * '=' characters. - */ - static List queryStringToNamesAndValues(String encodedQuery) { - List result = new ArrayList<>(); - for (int pos = 0; pos <= encodedQuery.length(); ) { - int ampersandOffset = encodedQuery.indexOf('&', pos); - if (ampersandOffset == -1) ampersandOffset = encodedQuery.length(); - - int equalsOffset = encodedQuery.indexOf('=', pos); - if (equalsOffset == -1 || equalsOffset > ampersandOffset) { - result.add(encodedQuery.substring(pos, ampersandOffset)); - result.add(null); // No value for this name. - } else { - result.add(encodedQuery.substring(pos, equalsOffset)); - result.add(encodedQuery.substring(equalsOffset + 1, ampersandOffset)); - } - pos = ampersandOffset + 1; - } - return result; - } - - public String query() { - if (queryNamesAndValues == null) return null; // No query. - StringBuilder result = new StringBuilder(); - namesAndValuesToQueryString(result, queryNamesAndValues); - return result.toString(); - } - - public int querySize() { - return queryNamesAndValues != null ? queryNamesAndValues.size() / 2 : 0; - } - - /** - * Returns the first query parameter named {@code name} decoded using UTF-8, or null if there is - * no such query parameter. - */ - public String queryParameter(String name) { - if (queryNamesAndValues == null) return null; - for (int i = 0, size = queryNamesAndValues.size(); i < size; i += 2) { - if (name.equals(queryNamesAndValues.get(i))) { - return queryNamesAndValues.get(i + 1); - } - } - return null; - } - - public Set queryParameterNames() { - if (queryNamesAndValues == null) return Collections.emptySet(); - Set result = new LinkedHashSet<>(); - for (int i = 0, size = queryNamesAndValues.size(); i < size; i += 2) { - result.add(queryNamesAndValues.get(i)); - } - return Collections.unmodifiableSet(result); - } - - public List queryParameterValues(String name) { - if (queryNamesAndValues == null) return Collections.emptyList(); - List result = new ArrayList<>(); - for (int i = 0, size = queryNamesAndValues.size(); i < size; i += 2) { - if (name.equals(queryNamesAndValues.get(i))) { - result.add(queryNamesAndValues.get(i + 1)); - } - } - return Collections.unmodifiableList(result); - } - - public String queryParameterName(int index) { - return queryNamesAndValues.get(index * 2); - } - - public String queryParameterValue(int index) { - return queryNamesAndValues.get(index * 2 + 1); - } - - public String encodedFragment() { - if (fragment == null) return null; - int fragmentStart = url.indexOf('#') + 1; - return url.substring(fragmentStart); - } - - public String fragment() { - return fragment; - } - - /** Returns the URL that would be retrieved by following {@code link} from this URL. */ - public HttpUrl resolve(String link) { - Builder builder = new Builder(); - Builder.ParseResult result = builder.parse(this, link); - return result == Builder.ParseResult.SUCCESS ? builder.build() : null; - } - public Builder newBuilder() { Builder result = new 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 = port != defaultPort(scheme) ? port : -1; - result.encodedPathSegments.clear(); - result.encodedPathSegments.addAll(encodedPathSegments()); - result.encodedQuery(encodedQuery()); - result.encodedFragment = encodedFragment(); return result; } - /** - * Returns a new {@code HttpUrl} representing {@code url} if it is a well-formed HTTP or HTTPS - * URL, or null if it isn't. - */ - public static HttpUrl parse(String url) { - Builder builder = new Builder(); - Builder.ParseResult result = builder.parse(null, url); - return result == Builder.ParseResult.SUCCESS ? builder.build() : null; - } - - /** - * Returns an {@link HttpUrl} for {@code url} if its protocol is {@code http} or {@code https}, or - * null if it has any other protocol. - */ - public static HttpUrl get(URL url) { - return parse(url.toString()); - } - - /** - * Returns a new {@code HttpUrl} representing {@code url} if it is a well-formed HTTP or HTTPS - * URL, or throws an exception if it isn't. - * - * @throws MalformedURLException if there was a non-host related URL issue - * @throws UnknownHostException if the host was invalid - */ - static HttpUrl getChecked(String url) throws MalformedURLException, UnknownHostException { - Builder builder = new Builder(); - Builder.ParseResult result = builder.parse(null, url); - switch (result) { - case SUCCESS: - return builder.build(); - case INVALID_HOST: - throw new UnknownHostException("Invalid host: " + url); - case UNSUPPORTED_SCHEME: - case MISSING_SCHEME: - case INVALID_PORT: - default: - throw new MalformedURLException("Invalid URL: " + result + " for " + url); - } - } - - public static HttpUrl get(URI uri) { - return parse(uri.toString()); - } - @Override public boolean equals(Object o) { return o instanceof HttpUrl && ((HttpUrl) o).url.equals(url); } @@ -650,16 +120,10 @@ public static HttpUrl get(URI uri) { public static final class Builder { String scheme; - String encodedUsername = ""; - String encodedPassword = ""; String host; int port = -1; - final List encodedPathSegments = new ArrayList<>(); - List encodedQueryNamesAndValues; - String encodedFragment; public Builder() { - encodedPathSegments.add(""); // The default path is '/' which needs a trailing space. } public Builder scheme(String scheme) { @@ -675,30 +139,6 @@ public Builder scheme(String scheme) { return this; } - public Builder username(String username) { - if (username == null) throw new IllegalArgumentException("username == null"); - this.encodedUsername = canonicalize(username, USERNAME_ENCODE_SET, false, false, true); - return this; - } - - public Builder encodedUsername(String encodedUsername) { - if (encodedUsername == null) throw new IllegalArgumentException("encodedUsername == null"); - this.encodedUsername = canonicalize(encodedUsername, USERNAME_ENCODE_SET, true, false, true); - return this; - } - - public Builder password(String password) { - if (password == null) throw new IllegalArgumentException("password == null"); - this.encodedPassword = canonicalize(password, PASSWORD_ENCODE_SET, false, false, true); - return this; - } - - public Builder encodedPassword(String encodedPassword) { - if (encodedPassword == null) throw new IllegalArgumentException("encodedPassword == null"); - this.encodedPassword = canonicalize(encodedPassword, PASSWORD_ENCODE_SET, true, false, true); - return this; - } - /** * @param host either a regular hostname, International Domain Name, IPv4 address, or IPv6 * address. @@ -721,181 +161,6 @@ int effectivePort() { return port != -1 ? port : defaultPort(scheme); } - public Builder addPathSegment(String pathSegment) { - if (pathSegment == null) throw new IllegalArgumentException("pathSegment == null"); - push(pathSegment, 0, pathSegment.length(), false, false); - return this; - } - - public Builder addEncodedPathSegment(String encodedPathSegment) { - if (encodedPathSegment == null) { - throw new IllegalArgumentException("encodedPathSegment == null"); - } - push(encodedPathSegment, 0, encodedPathSegment.length(), false, true); - return this; - } - - public Builder setPathSegment(int index, String pathSegment) { - if (pathSegment == null) throw new IllegalArgumentException("pathSegment == null"); - String canonicalPathSegment = canonicalize( - pathSegment, 0, pathSegment.length(), PATH_SEGMENT_ENCODE_SET, false, false, true); - if (isDot(canonicalPathSegment) || isDotDot(canonicalPathSegment)) { - throw new IllegalArgumentException("unexpected path segment: " + pathSegment); - } - encodedPathSegments.set(index, canonicalPathSegment); - return this; - } - - public Builder setEncodedPathSegment(int index, String encodedPathSegment) { - if (encodedPathSegment == null) { - throw new IllegalArgumentException("encodedPathSegment == null"); - } - String canonicalPathSegment = canonicalize(encodedPathSegment, - 0, encodedPathSegment.length(), PATH_SEGMENT_ENCODE_SET, true, false, true); - encodedPathSegments.set(index, canonicalPathSegment); - if (isDot(canonicalPathSegment) || isDotDot(canonicalPathSegment)) { - throw new IllegalArgumentException("unexpected path segment: " + encodedPathSegment); - } - return this; - } - - public Builder removePathSegment(int index) { - encodedPathSegments.remove(index); - if (encodedPathSegments.isEmpty()) { - encodedPathSegments.add(""); // Always leave at least one '/'. - } - return this; - } - - public Builder encodedPath(String encodedPath) { - if (encodedPath == null) throw new IllegalArgumentException("encodedPath == null"); - if (!encodedPath.startsWith("/")) { - throw new IllegalArgumentException("unexpected encodedPath: " + encodedPath); - } - resolvePath(encodedPath, 0, encodedPath.length()); - return this; - } - - public Builder query(String query) { - this.encodedQueryNamesAndValues = query != null - ? queryStringToNamesAndValues(canonicalize(query, QUERY_ENCODE_SET, false, true, true)) - : null; - return this; - } - - public Builder encodedQuery(String encodedQuery) { - this.encodedQueryNamesAndValues = encodedQuery != null - ? queryStringToNamesAndValues( - canonicalize(encodedQuery, QUERY_ENCODE_SET, true, true, true)) - : null; - return this; - } - - /** Encodes the query parameter using UTF-8 and adds it to this URL's query string. */ - public Builder addQueryParameter(String name, String value) { - if (name == null) throw new IllegalArgumentException("name == null"); - if (encodedQueryNamesAndValues == null) encodedQueryNamesAndValues = new ArrayList<>(); - encodedQueryNamesAndValues.add( - canonicalize(name, QUERY_COMPONENT_ENCODE_SET, false, true, true)); - encodedQueryNamesAndValues.add(value != null - ? canonicalize(value, QUERY_COMPONENT_ENCODE_SET, false, true, true) - : null); - return this; - } - - /** Adds the pre-encoded query parameter to this URL's query string. */ - public Builder addEncodedQueryParameter(String encodedName, String encodedValue) { - if (encodedName == null) throw new IllegalArgumentException("encodedName == null"); - if (encodedQueryNamesAndValues == null) encodedQueryNamesAndValues = new ArrayList<>(); - encodedQueryNamesAndValues.add( - canonicalize(encodedName, QUERY_COMPONENT_ENCODE_SET, true, true, true)); - encodedQueryNamesAndValues.add(encodedValue != null - ? canonicalize(encodedValue, QUERY_COMPONENT_ENCODE_SET, true, true, true) - : null); - return this; - } - - public Builder setQueryParameter(String name, String value) { - removeAllQueryParameters(name); - addQueryParameter(name, value); - return this; - } - - public Builder setEncodedQueryParameter(String encodedName, String encodedValue) { - removeAllEncodedQueryParameters(encodedName); - addEncodedQueryParameter(encodedName, encodedValue); - return this; - } - - public Builder removeAllQueryParameters(String name) { - if (name == null) throw new IllegalArgumentException("name == null"); - if (encodedQueryNamesAndValues == null) return this; - String nameToRemove = canonicalize(name, QUERY_COMPONENT_ENCODE_SET, false, true, true); - removeAllCanonicalQueryParameters(nameToRemove); - return this; - } - - public Builder removeAllEncodedQueryParameters(String encodedName) { - if (encodedName == null) throw new IllegalArgumentException("encodedName == null"); - if (encodedQueryNamesAndValues == null) return this; - removeAllCanonicalQueryParameters( - canonicalize(encodedName, QUERY_COMPONENT_ENCODE_SET, true, true, true)); - return this; - } - - private void removeAllCanonicalQueryParameters(String canonicalName) { - for (int i = encodedQueryNamesAndValues.size() - 2; i >= 0; i -= 2) { - if (canonicalName.equals(encodedQueryNamesAndValues.get(i))) { - encodedQueryNamesAndValues.remove(i + 1); - encodedQueryNamesAndValues.remove(i); - if (encodedQueryNamesAndValues.isEmpty()) { - encodedQueryNamesAndValues = null; - return; - } - } - } - } - - public Builder fragment(String fragment) { - this.encodedFragment = fragment != null - ? canonicalize(fragment, FRAGMENT_ENCODE_SET, false, false, false) - : null; - return this; - } - - public Builder encodedFragment(String encodedFragment) { - this.encodedFragment = encodedFragment != null - ? canonicalize(encodedFragment, FRAGMENT_ENCODE_SET, true, false, false) - : null; - return this; - } - - /** - * Re-encodes the components of this URL so that it satisfies (obsolete) RFC 2396, which is - * particularly strict for certain components. - */ - Builder reencodeForUri() { - for (int i = 0, size = encodedPathSegments.size(); i < size; i++) { - String pathSegment = encodedPathSegments.get(i); - encodedPathSegments.set(i, - canonicalize(pathSegment, PATH_SEGMENT_ENCODE_SET_URI, true, false, true)); - } - if (encodedQueryNamesAndValues != null) { - for (int i = 0, size = encodedQueryNamesAndValues.size(); i < size; i++) { - String component = encodedQueryNamesAndValues.get(i); - if (component != null) { - encodedQueryNamesAndValues.set(i, - canonicalize(component, QUERY_COMPONENT_ENCODE_SET_URI, true, true, true)); - } - } - } - if (encodedFragment != null) { - encodedFragment = canonicalize( - encodedFragment, FRAGMENT_ENCODE_SET_URI, true, false, false); - } - return this; - } - public HttpUrl build() { if (scheme == null) throw new IllegalStateException("scheme == null"); if (host == null) throw new IllegalStateException("host == null"); @@ -907,15 +172,6 @@ public HttpUrl build() { result.append(scheme); result.append("://"); - if (!encodedUsername.isEmpty() || !encodedPassword.isEmpty()) { - result.append(encodedUsername); - if (!encodedPassword.isEmpty()) { - result.append(':'); - result.append(encodedPassword); - } - result.append('@'); - } - if (host.indexOf(':') != -1) { // Host is an IPv6 address. result.append('['); @@ -931,332 +187,9 @@ public HttpUrl build() { result.append(effectivePort); } - pathSegmentsToString(result, encodedPathSegments); - - if (encodedQueryNamesAndValues != null) { - result.append('?'); - namesAndValuesToQueryString(result, encodedQueryNamesAndValues); - } - - if (encodedFragment != null) { - result.append('#'); - result.append(encodedFragment); - } - return result.toString(); } - enum ParseResult { - SUCCESS, - MISSING_SCHEME, - UNSUPPORTED_SCHEME, - INVALID_PORT, - INVALID_HOST, - } - - ParseResult parse(HttpUrl base, String input) { - int pos = skipLeadingAsciiWhitespace(input, 0, input.length()); - int limit = skipTrailingAsciiWhitespace(input, pos, input.length()); - - // Scheme. - int schemeDelimiterOffset = schemeDelimiterOffset(input, pos, limit); - if (schemeDelimiterOffset != -1) { - if (input.regionMatches(true, pos, "https:", 0, 6)) { - this.scheme = "https"; - pos += "https:".length(); - } else if (input.regionMatches(true, pos, "http:", 0, 5)) { - this.scheme = "http"; - pos += "http:".length(); - } else { - return ParseResult.UNSUPPORTED_SCHEME; // Not an HTTP scheme. - } - } else if (base != null) { - this.scheme = base.scheme; - } else { - return ParseResult.MISSING_SCHEME; // No scheme. - } - - // Authority. - boolean hasUsername = false; - boolean hasPassword = false; - int slashCount = slashCount(input, pos, limit); - if (slashCount >= 2 || base == null || !base.scheme.equals(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) { - int componentDelimiterOffset = delimiterOffset(input, pos, limit, "@/\\?#"); - int c = componentDelimiterOffset != limit - ? input.charAt(componentDelimiterOffset) - : -1; - switch (c) { - case '@': - // User info precedes. - if (!hasPassword) { - int passwordColonOffset = delimiterOffset( - input, pos, componentDelimiterOffset, ":"); - String canonicalUsername = canonicalize( - input, pos, passwordColonOffset, USERNAME_ENCODE_SET, true, false, true); - this.encodedUsername = hasUsername - ? this.encodedUsername + "%40" + canonicalUsername - : canonicalUsername; - if (passwordColonOffset != componentDelimiterOffset) { - hasPassword = true; - this.encodedPassword = canonicalize(input, passwordColonOffset + 1, - componentDelimiterOffset, PASSWORD_ENCODE_SET, true, false, true); - } - hasUsername = true; - } else { - this.encodedPassword = this.encodedPassword + "%40" + canonicalize( - input, pos, componentDelimiterOffset, PASSWORD_ENCODE_SET, true, false, true); - } - pos = componentDelimiterOffset + 1; - break; - - case -1: - case '/': - case '\\': - case '?': - case '#': - // Host info precedes. - int portColonOffset = portColonOffset(input, pos, componentDelimiterOffset); - if (portColonOffset + 1 < componentDelimiterOffset) { - this.host = canonicalizeHost(input, pos, portColonOffset); - this.port = parsePort(input, portColonOffset + 1, componentDelimiterOffset); - if (this.port == -1) return ParseResult.INVALID_PORT; // Invalid port. - } else { - this.host = canonicalizeHost(input, pos, portColonOffset); - this.port = defaultPort(this.scheme); - } - if (this.host == null) return ParseResult.INVALID_HOST; // Invalid host. - 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.charAt(pos) == '#') { - encodedQuery(base.encodedQuery()); - } - } - - // Resolve the relative path. - int pathDelimiterOffset = delimiterOffset(input, pos, limit, "?#"); - resolvePath(input, pos, pathDelimiterOffset); - pos = pathDelimiterOffset; - - // Query. - if (pos < limit && input.charAt(pos) == '?') { - int queryDelimiterOffset = delimiterOffset(input, pos, limit, "#"); - this.encodedQueryNamesAndValues = queryStringToNamesAndValues(canonicalize( - input, pos + 1, queryDelimiterOffset, QUERY_ENCODE_SET, true, true, true)); - pos = queryDelimiterOffset; - } - - // Fragment. - if (pos < limit && input.charAt(pos) == '#') { - this.encodedFragment = canonicalize( - input, pos + 1, limit, FRAGMENT_ENCODE_SET, true, false, false); - } - - return ParseResult.SUCCESS; - } - - private void resolvePath(String input, int pos, int limit) { - // Read a delimiter. - if (pos == limit) { - // Empty path: keep the base path as-is. - return; - } - char c = input.charAt(pos); - if (c == '/' || c == '\\') { - // Absolute path: reset to the default "/". - encodedPathSegments.clear(); - encodedPathSegments.add(""); - pos++; - } else { - // Relative path: clear everything after the last '/'. - encodedPathSegments.set(encodedPathSegments.size() - 1, ""); - } - - // Read path segments. - for (int i = pos; i < limit; ) { - int pathSegmentDelimiterOffset = delimiterOffset(input, i, limit, "/\\"); - boolean segmentHasTrailingSlash = pathSegmentDelimiterOffset < limit; - push(input, i, pathSegmentDelimiterOffset, segmentHasTrailingSlash, true); - i = pathSegmentDelimiterOffset; - if (segmentHasTrailingSlash) i++; - } - } - - /** Adds a path segment. If the input is ".." or equivalent, this pops a path segment. */ - private void push(String input, int pos, int limit, boolean addTrailingSlash, - boolean alreadyEncoded) { - String segment = canonicalize( - input, pos, limit, PATH_SEGMENT_ENCODE_SET, alreadyEncoded, false, true); - if (isDot(segment)) { - return; // Skip '.' path segments. - } - if (isDotDot(segment)) { - pop(); - return; - } - if (encodedPathSegments.get(encodedPathSegments.size() - 1).isEmpty()) { - encodedPathSegments.set(encodedPathSegments.size() - 1, segment); - } else { - encodedPathSegments.add(segment); - } - if (addTrailingSlash) { - encodedPathSegments.add(""); - } - } - - private boolean isDot(String input) { - return input.equals(".") || input.equalsIgnoreCase("%2e"); - } - - private boolean isDotDot(String input) { - return input.equals("..") - || input.equalsIgnoreCase("%2e.") - || input.equalsIgnoreCase(".%2e") - || input.equalsIgnoreCase("%2e%2e"); - } - - /** - * 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", ""]. - */ - private void pop() { - String removed = encodedPathSegments.remove(encodedPathSegments.size() - 1); - - // Make sure the path ends with a '/' by either adding an empty string or clearing a segment. - if (removed.isEmpty() && !encodedPathSegments.isEmpty()) { - encodedPathSegments.set(encodedPathSegments.size() - 1, ""); - } else { - encodedPathSegments.add(""); - } - } - - /** - * Increments {@code pos} until {@code input[pos]} is not ASCII whitespace. Stops at {@code - * limit}. - */ - private int skipLeadingAsciiWhitespace(String input, int pos, int limit) { - for (int i = pos; i < limit; i++) { - switch (input.charAt(i)) { - case '\t': - case '\n': - case '\f': - case '\r': - case ' ': - continue; - default: - return i; - } - } - return limit; - } - - /** - * Decrements {@code limit} until {@code input[limit - 1]} is not ASCII whitespace. Stops at - * {@code pos}. - */ - private int skipTrailingAsciiWhitespace(String input, int pos, int limit) { - for (int i = limit - 1; i >= pos; i--) { - switch (input.charAt(i)) { - case '\t': - case '\n': - case '\f': - case '\r': - case ' ': - continue; - default: - return i + 1; - } - } - return pos; - } - - /** - * Returns the index of the ':' in {@code input} that is after scheme characters. Returns -1 if - * {@code input} does not have a scheme that starts at {@code pos}. - */ - private static int schemeDelimiterOffset(String input, int pos, int limit) { - if (limit - pos < 2) return -1; - - char c0 = input.charAt(pos); - if ((c0 < 'a' || c0 > 'z') && (c0 < 'A' || c0 > 'Z')) return -1; // Not a scheme start char. - - for (int i = pos + 1; i < limit; i++) { - char c = input.charAt(i); - - if ((c >= 'a' && c <= 'z') - || (c >= 'A' && c <= 'Z') - || (c >= '0' && c <= '9') - || c == '+' - || c == '-' - || c == '.') { - continue; // Scheme character. Keep going. - } else if (c == ':') { - return i; // Scheme prefix! - } else { - return -1; // Non-scheme character before the first ':'. - } - } - - return -1; // No ':'; doesn't start with a scheme. - } - - /** Returns the number of '/' and '\' slashes in {@code input}, starting at {@code pos}. */ - private static int slashCount(String input, int pos, int limit) { - int slashCount = 0; - while (pos < limit) { - char c = input.charAt(pos); - if (c == '\\' || c == '/') { - slashCount++; - pos++; - } else { - break; - } - } - return slashCount; - } - - /** Finds the first ':' in {@code input}, skipping characters between square braces "[...]". */ - private static int portColonOffset(String input, int pos, int limit) { - for (int i = pos; i < limit; i++) { - switch (input.charAt(i)) { - case '[': - while (++i < limit) { - if (input.charAt(i) == ']') break; - } - break; - case ':': - return i; - } - } - return limit; // No colon. - } private static String canonicalizeHost(String input, int pos, int limit) { // Start by percent decoding the host. The WHATWG spec suggests doing this only after we've @@ -1348,7 +281,7 @@ private static InetAddress decodeIpv6(String input, int pos, int limit) { /** Decodes an IPv4 address suffix of an IPv6 address, like 1111::5555:6666:192.168.0.1. */ private static boolean decodeIpv4Suffix( - String input, int pos, int limit, byte[] address, int addressOffset) { + String input, int pos, int limit, byte[] address, int addressOffset) { int b = addressOffset; for (int i = pos; i < limit; ) { @@ -1447,48 +380,13 @@ private static String inet6AddressToAscii(byte[] address) { if (i == 16) result.writeByte(':'); } else { if (i > 0) result.writeByte(':'); - int group = (address[i] & 0xff) << 8 | address[i + 1] & 0xff; + int group = (address[i] & 0xff) << 8 | (address[i + 1] & 0xff); result.writeHexadecimalUnsignedLong(group); i += 2; } } return result.readUtf8(); } - - private static int parsePort(String input, int pos, int limit) { - try { - // Canonicalize the port string to skip '\n' etc. - String portString = canonicalize(input, pos, limit, "", false, false, true); - int i = Integer.parseInt(portString); - if (i > 0 && i <= 65535) return i; - return -1; - } catch (NumberFormatException e) { - return -1; // Invalid port. - } - } - } - - /** - * Returns the index of the first character in {@code input} that contains a character in {@code - * delimiters}. Returns limit if there is no such character. - */ - private static int delimiterOffset(String input, int pos, int limit, String delimiters) { - for (int i = pos; i < limit; i++) { - if (delimiters.indexOf(input.charAt(i)) != -1) return i; - } - return limit; - } - - static String percentDecode(String encoded, boolean plusIsSpace) { - return percentDecode(encoded, 0, encoded.length(), plusIsSpace); - } - - private List percentDecode(List list, boolean plusIsSpace) { - List result = new ArrayList<>(list.size()); - for (String s : list) { - result.add(s != null ? percentDecode(s, plusIsSpace) : null); - } - return Collections.unmodifiableList(result); } static String percentDecode(String encoded, int pos, int limit, boolean plusIsSpace) { @@ -1534,61 +432,23 @@ static int decodeHexDigit(char c) { return -1; } - /** - * Returns a substring of {@code input} on the range {@code [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 {@code 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 plusIsSpace true to encode '+' as "%2B" if it is not already encoded. - * @param asciiOnly true to encode all non-ASCII codepoints. - */ - static String canonicalize(String input, int pos, int limit, String encodeSet, - boolean alreadyEncoded, boolean plusIsSpace, boolean asciiOnly) { - int codePoint; - for (int i = pos; i < limit; i += Character.charCount(codePoint)) { - codePoint = input.codePointAt(i); - if (codePoint < 0x20 - || codePoint == 0x7f - || (codePoint >= 0x80 && asciiOnly) - || encodeSet.indexOf(codePoint) != -1 - || (codePoint == '%' && !alreadyEncoded) - || (codePoint == '+' && plusIsSpace)) { - // Slow path: the character at i requires encoding! - Buffer out = new Buffer(); - out.writeUtf8(input, pos, i); - canonicalize(out, input, i, limit, encodeSet, alreadyEncoded, plusIsSpace, asciiOnly); - return out.readUtf8(); - } - } - - // Fast path: no characters in [pos..limit) required encoding. - return input.substring(pos, limit); - } - static void canonicalize(Buffer out, String input, int pos, int limit, - String encodeSet, boolean alreadyEncoded, boolean plusIsSpace, boolean asciiOnly) { + String encodeSet, boolean alreadyEncoded, boolean plusIsSpace, boolean asciiOnly) { Buffer utf8Buffer = null; // Lazily allocated. int codePoint; for (int i = pos; i < limit; i += Character.charCount(codePoint)) { codePoint = input.codePointAt(i); if (alreadyEncoded - && (codePoint == '\t' || codePoint == '\n' || codePoint == '\f' || codePoint == '\r')) { + && (codePoint == '\t' || codePoint == '\n' || codePoint == '\f' || codePoint == '\r')) { // Skip this character. } else if (codePoint == '+' && plusIsSpace) { // Encode '+' as '%2B' since we permit ' ' to be encoded as either '+' or '%20'. out.writeUtf8(alreadyEncoded ? "+" : "%2B"); } else if (codePoint < 0x20 - || codePoint == 0x7f - || (codePoint >= 0x80 && asciiOnly) - || encodeSet.indexOf(codePoint) != -1 - || (codePoint == '%' && !alreadyEncoded)) { + || codePoint == 0x7f + || (codePoint >= 0x80 && asciiOnly) + || encodeSet.indexOf(codePoint) != -1 + || (codePoint == '%' && !alreadyEncoded)) { // Percent encode this character. if (utf8Buffer == null) { utf8Buffer = new Buffer(); @@ -1606,10 +466,4 @@ static void canonicalize(Buffer out, String input, int pos, int limit, } } } - - static String canonicalize(String input, String encodeSet, boolean alreadyEncoded, - boolean plusIsSpace, boolean asciiOnly) { - return canonicalize( - input, 0, input.length(), encodeSet, alreadyEncoded, plusIsSpace, asciiOnly); - } } diff --git a/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/proxy/Request.java b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/proxy/Request.java index ce6bd18113e..aef4f3783d5 100644 --- a/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/proxy/Request.java +++ b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/proxy/Request.java @@ -14,133 +14,49 @@ * limitations under the License. */ /* - * Forked from OkHttp 2.7.0 + * Forked from OkHttp 2.7.0 com.squareup.okhttp.Request */ - package io.grpc.okhttp.internal.proxy; -import com.squareup.okhttp.internal.http.HttpMethod; import io.grpc.okhttp.internal.Headers; -import java.io.IOException; -import java.net.URI; -import java.net.URL; -import java.util.List; - /** - * An HTTP request. Instances of this class are immutable if their {@link #body} - * is null or itself immutable. + * An HTTP ProxyRequest. Instances of this class are immutable. */ public final class Request { private final HttpUrl url; - private final String method; private final Headers headers; - private final RequestBody body; - private final Object tag; - - private volatile URL javaNetUrl; // Lazily initialized. - private volatile URI javaNetUri; // Lazily initialized. - private volatile CacheControl cacheControl; // Lazily initialized. private Request(Builder builder) { this.url = builder.url; - this.method = builder.method; this.headers = builder.headers.build(); - this.body = builder.body; - this.tag = builder.tag != null ? builder.tag : this; } public HttpUrl httpUrl() { return url; } - public URL url() { - URL result = javaNetUrl; - return result != null ? result : (javaNetUrl = url.url()); - } - - public URI uri() throws IOException { - try { - URI result = javaNetUri; - return result != null ? result : (javaNetUri = url.uri()); - } catch (IllegalStateException e) { - throw new IOException(e.getMessage()); - } - } - - public String urlString() { - return url.toString(); - } - - public String method() { - return method; - } - public Headers headers() { return headers; } - public String header(String name) { - return headers.get(name); - } - - public List headers(String name) { - return headers.values(name); - } - - public RequestBody body() { - return body; - } - - public Object tag() { - return tag; - } - public Builder newBuilder() { - return new Builder(this); + return new Builder(); } - /** - * Returns the cache control directives for this response. This is never null, - * even if this response contains no {@code Cache-Control} header. - */ - public CacheControl cacheControl() { - CacheControl result = cacheControl; - return result != null ? result : (cacheControl = CacheControl.parse(headers)); - } - - public boolean isHttps() { - return url.isHttps(); - } @Override public String toString() { - return "Request{method=" - + method - + ", url=" - + url - + ", tag=" - + (tag != this ? tag : null) - + '}'; + return "Request{" + + "url=" + url + + '}'; } public static class Builder { private HttpUrl url; - private String method; private Headers.Builder headers; - private RequestBody body; - private Object tag; public Builder() { - this.method = "GET"; - this.headers = new Headers.Builder(); - } - - private Builder(Request request) { - this.url = request.url; - this.method = request.method; - this.body = request.body; - this.tag = request.tag; - this.headers = request.headers.newBuilder(); + this.headers = new Headers.Builder(); } public Builder url(HttpUrl url) { @@ -149,40 +65,6 @@ public Builder url(HttpUrl url) { return this; } - /** - * Sets the URL target of this request. - * - * @throws IllegalArgumentException if {@code url} is not a valid HTTP or HTTPS URL. Avoid this - * exception by calling {@link HttpUrl#parse}; it returns null for invalid URLs. - */ - public Builder url(String url) { - if (url == null) throw new IllegalArgumentException("url == null"); - - // Silently replace websocket URLs with HTTP URLs. - if (url.regionMatches(true, 0, "ws:", 0, 3)) { - url = "http:" + url.substring(3); - } else if (url.regionMatches(true, 0, "wss:", 0, 4)) { - url = "https:" + url.substring(4); - } - - HttpUrl parsed = HttpUrl.parse(url); - if (parsed == null) throw new IllegalArgumentException("unexpected url: " + url); - return url(parsed); - } - - /** - * Sets the URL target of this request. - * - * @throws IllegalArgumentException if the scheme of {@code url} is not {@code http} or {@code - * https}. - */ - public Builder url(URL url) { - if (url == null) throw new IllegalArgumentException("url == null"); - HttpUrl parsed = HttpUrl.get(url); - if (parsed == null) throw new IllegalArgumentException("unexpected url: " + url); - return url(parsed); - } - /** * Sets the header named {@code name} to {@code value}. If this request * already has any headers with that name, they are all replaced. @@ -192,93 +74,6 @@ public Builder header(String name, String value) { return this; } - /** - * Adds a header with {@code name} and {@code value}. Prefer this method for - * multiply-valued headers like "Cookie". - * - *

Note that for some headers including {@code Content-Length} and {@code Content-Encoding}, - * OkHttp may replace {@code value} with a header derived from the request body. - */ - public Builder addHeader(String name, String value) { - headers.add(name, value); - return this; - } - - public Builder removeHeader(String name) { - headers.removeAll(name); - return this; - } - - /** Removes all headers on this builder and adds {@code headers}. */ - public Builder headers(Headers headers) { - this.headers = headers.newBuilder(); - return this; - } - - /** - * Sets this request's {@code Cache-Control} header, replacing any cache - * control headers already present. If {@code cacheControl} doesn't define - * any directives, this clears this request's cache-control headers. - */ - public Builder cacheControl(CacheControl cacheControl) { - String value = cacheControl.toString(); - if (value.isEmpty()) return removeHeader("Cache-Control"); - return header("Cache-Control", value); - } - - public Builder get() { - return method("GET", null); - } - - public Builder head() { - return method("HEAD", null); - } - - public Builder post(RequestBody body) { - return method("POST", body); - } - - public Builder delete(RequestBody body) { - return method("DELETE", body); - } - - public Builder delete() { - return delete(RequestBody.create(null, new byte[0])); - } - - public Builder put(RequestBody body) { - return method("PUT", body); - } - - public Builder patch(RequestBody body) { - return method("PATCH", body); - } - - public Builder method(String method, RequestBody body) { - if (method == null || method.length() == 0) { - throw new IllegalArgumentException("method == null || method.length() == 0"); - } - if (body != null && !HttpMethod.permitsRequestBody(method)) { - throw new IllegalArgumentException("method " + method + " must not have a request body."); - } - if (body == null && HttpMethod.requiresRequestBody(method)) { - throw new IllegalArgumentException("method " + method + " must have a request body."); - } - this.method = method; - this.body = body; - return this; - } - - /** - * Attaches {@code tag} to the request. It can be used later to cancel the - * request. If the tag is unspecified or null, the request is canceled by - * using the request itself as the tag. - */ - public Builder tag(Object tag) { - this.tag = tag; - return this; - } - public Request build() { if (url == null) throw new IllegalStateException("url == null"); return new Request(this);