From ef664405e5018dae6e017797a3f614d465376d9d Mon Sep 17 00:00:00 2001 From: beatrausch <30717474+beatrausch@users.noreply.github.com> Date: Wed, 30 Mar 2022 22:43:30 +0200 Subject: [PATCH] okhttp: make okhttp dependencies compile only (#8971) * okhttp: forked required files to make okhttp dep compile only * okhttp: forked missing file to make okhttp dep compile only * okhttp: moved url and request files to proxy packge * okhttp: removed unused methods from forked files; fixed build --- interop-testing/build.gradle | 3 +- okhttp/build.gradle | 8 +- .../io/grpc/okhttp/OkHttpClientTransport.java | 11 +- .../io/grpc/okhttp/internal/Credentials.java | 40 ++ .../java/io/grpc/okhttp/internal/Headers.java | 152 ++++++ .../io/grpc/okhttp/internal/StatusLine.java | 87 ++++ .../grpc/okhttp/internal/proxy/HttpUrl.java | 469 ++++++++++++++++++ .../grpc/okhttp/internal/proxy/Request.java | 82 +++ 8 files changed, 843 insertions(+), 9 deletions(-) create mode 100644 okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/Credentials.java 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/StatusLine.java create mode 100644 okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/proxy/HttpUrl.java create mode 100644 okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/proxy/Request.java diff --git a/interop-testing/build.gradle b/interop-testing/build.gradle index 5a13c6161437..cc66b4463a44 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 999f21e7c103..490e527cbf11 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 cf7cae19d8a9..e233fa20028c 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/Credentials.java b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/Credentials.java new file mode 100644 index 000000000000..08a46ada7a77 --- /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(); + } + } +} 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 000000000000..4723a87dfbea --- /dev/null +++ b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/Headers.java @@ -0,0 +1,152 @@ +/* + * 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 com.squareup.okhttp.Headers + */ +package io.grpc.okhttp.internal; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * The header fields of a single HTTP message. Values are uninterpreted strings; + * + *

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()]); + } + + /** Returns the last value corresponding to the specified field, or null. */ + public String get(String name) { + return get(namesAndValues, name); + } + + /** 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]; + } + + 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(); + } + + 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; + } + + public static final class Builder { + private final List namesAndValues = new ArrayList<>(20); + + /** + * 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)); + } + } + } + + 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 new file mode 100644 index 000000000000..ab72ee2d2947 --- /dev/null +++ b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/StatusLine.java @@ -0,0 +1,87 @@ +/* + * Forked from OkHttp 2.7.0 + */ +package io.grpc.okhttp.internal; + +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 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(); + } +} 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 new file mode 100644 index 000000000000..31003c02ca16 --- /dev/null +++ b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/proxy/HttpUrl.java @@ -0,0 +1,469 @@ +/* + * 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 com.squareup.okhttp.HttpUrl + */ +package io.grpc.okhttp.internal.proxy; + +import java.net.IDN; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.Locale; +import okio.Buffer; + +/** + * 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' }; + + /** Either "http" or "https". */ + private final String scheme; + + /** Canonical hostname. */ + private final String host; + + /** Either 80, 443 or a user-specified port. In range [1..65535]. */ + private final int port; + + /** Canonical URL. */ + private final String url; + + private HttpUrl(Builder builder) { + this.scheme = builder.scheme; + this.host = builder.host; + this.port = builder.effectivePort(); + this.url = builder.toString(); + } + + /** Returns either "http" or "https". */ + public String scheme() { + return scheme; + } + + public boolean isHttps() { + return scheme.equals("https"); + } + + /** + * 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 Builder newBuilder() { + Builder result = new Builder(); + result.scheme = scheme; + 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; + return result; + } + + @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 host; + int port = -1; + + public Builder() { + } + + 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; + } + + /** + * @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 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 (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); + } + + return result.toString(); + } + + + 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(); + } + } + + 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; + } + + 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); + } + } + } +} 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 new file mode 100644 index 000000000000..aef4f3783d5a --- /dev/null +++ b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/proxy/Request.java @@ -0,0 +1,82 @@ +/* + * 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 com.squareup.okhttp.Request + */ +package io.grpc.okhttp.internal.proxy; + +import io.grpc.okhttp.internal.Headers; + +/** + * An HTTP ProxyRequest. Instances of this class are immutable. + */ +public final class Request { + private final HttpUrl url; + private final Headers headers; + + private Request(Builder builder) { + this.url = builder.url; + this.headers = builder.headers.build(); + } + + public HttpUrl httpUrl() { + return url; + } + + public Headers headers() { + return headers; + } + + public Builder newBuilder() { + return new Builder(); + } + + + @Override public String toString() { + return "Request{" + + "url=" + url + + '}'; + } + + public static class Builder { + private HttpUrl url; + private Headers.Builder headers; + + public Builder() { + this.headers = new Headers.Builder(); + } + + public Builder url(HttpUrl url) { + if (url == null) throw new IllegalArgumentException("url == null"); + this.url = url; + return this; + } + + /** + * 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; + } + + public Request build() { + if (url == null) throw new IllegalStateException("url == null"); + return new Request(this); + } + } +}