1
0
mirror of https://github.com/square/okhttp.git synced 2025-07-31 05:04:26 +03:00

Convert more tests from Java to Kotlin (#8155)

This commit is contained in:
Jesse Wilson
2023-12-23 03:26:25 -07:00
committed by GitHub
parent 1561bbaeae
commit 9724956cb4
87 changed files with 6628 additions and 7249 deletions

View File

@ -1,635 +0,0 @@
/*
* Copyright (C) 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.mockwebserver;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.net.ssl.HttpsURLConnection;
import okhttp3.Handshake;
import okhttp3.Headers;
import okhttp3.HttpUrl;
import okhttp3.Protocol;
import okhttp3.RecordingHostnameVerifier;
import okhttp3.TestUtil;
import okhttp3.testing.PlatformRule;
import okhttp3.tls.HandshakeCertificates;
import okhttp3.tls.HeldCertificate;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Arrays.asList;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.data.Offset.offset;
import static org.junit.jupiter.api.Assertions.fail;
@SuppressWarnings({"ArraysAsListWithZeroOrOneArgument", "deprecation"})
@Timeout(30)
@Tag("Slow")
public final class MockWebServerTest {
@RegisterExtension public PlatformRule platform = new PlatformRule();
private final MockWebServer server = new MockWebServer();
@BeforeEach public void setUp() throws IOException {
server.start();
}
@AfterEach
public void tearDown() throws Exception {
server.shutdown();
}
@Test public void defaultMockResponse() {
MockResponse response = new MockResponse();
assertThat(headersToList(response)).containsExactly("Content-Length: 0");
assertThat(response.getStatus()).isEqualTo("HTTP/1.1 200 OK");
}
@Test public void setResponseMockReason() {
String[] reasons = {
"Mock Response",
"Informational",
"OK",
"Redirection",
"Client Error",
"Server Error",
"Mock Response"
};
for (int i = 0; i < 600; i++) {
MockResponse response = new MockResponse().setResponseCode(i);
String expectedReason = reasons[i / 100];
assertThat(response.getStatus()).isEqualTo(("HTTP/1.1 " + i + " " + expectedReason));
assertThat(headersToList(response)).containsExactly("Content-Length: 0");
}
}
@Test public void setStatusControlsWholeStatusLine() {
MockResponse response = new MockResponse().setStatus("HTTP/1.1 202 That'll do pig");
assertThat(headersToList(response)).containsExactly("Content-Length: 0");
assertThat(response.getStatus()).isEqualTo("HTTP/1.1 202 That'll do pig");
}
@Test public void setBodyAdjustsHeaders() throws IOException {
MockResponse response = new MockResponse().setBody("ABC");
assertThat(headersToList(response)).containsExactly("Content-Length: 3");
assertThat(response.getBody().readUtf8()).isEqualTo("ABC");
}
@Test public void mockResponseAddHeader() {
MockResponse response = new MockResponse()
.clearHeaders()
.addHeader("Cookie: s=square")
.addHeader("Cookie", "a=android");
assertThat(headersToList(response)).containsExactly("Cookie: s=square", "Cookie: a=android");
}
@Test public void mockResponseSetHeader() {
MockResponse response = new MockResponse()
.clearHeaders()
.addHeader("Cookie: s=square")
.addHeader("Cookie: a=android")
.addHeader("Cookies: delicious");
response.setHeader("cookie", "r=robot");
assertThat(headersToList(response)).containsExactly("Cookies: delicious", "cookie: r=robot");
}
@Test public void mockResponseSetHeaders() {
MockResponse response = new MockResponse()
.clearHeaders()
.addHeader("Cookie: s=square")
.addHeader("Cookies: delicious");
response.setHeaders(new Headers.Builder().add("Cookie", "a=android").build());
assertThat(headersToList(response)).containsExactly("Cookie: a=android");
}
@Test public void regularResponse() throws Exception {
server.enqueue(new MockResponse().setBody("hello world"));
URL url = server.url("/").url();
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestProperty("Accept-Language", "en-US");
InputStream in = connection.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in, UTF_8));
assertThat(connection.getResponseCode()).isEqualTo(HttpURLConnection.HTTP_OK);
assertThat(reader.readLine()).isEqualTo("hello world");
RecordedRequest request = server.takeRequest();
assertThat(request.getRequestLine()).isEqualTo("GET / HTTP/1.1");
assertThat(request.getHeader("Accept-Language")).isEqualTo("en-US");
// Server has no more requests.
assertThat(server.takeRequest(100, MILLISECONDS)).isNull();
}
@Test public void redirect() throws Exception {
server.enqueue(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
.addHeader("Location: " + server.url("/new-path"))
.setBody("This page has moved!"));
server.enqueue(new MockResponse().setBody("This is the new location!"));
URLConnection connection = server.url("/").url().openConnection();
InputStream in = connection.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in, UTF_8));
assertThat(reader.readLine()).isEqualTo("This is the new location!");
RecordedRequest first = server.takeRequest();
assertThat(first.getRequestLine()).isEqualTo("GET / HTTP/1.1");
RecordedRequest redirect = server.takeRequest();
assertThat(redirect.getRequestLine()).isEqualTo("GET /new-path HTTP/1.1");
}
/**
* Test that MockWebServer blocks for a call to enqueue() if a request is made before a mock
* response is ready.
*/
@Test public void dispatchBlocksWaitingForEnqueue() throws Exception {
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
}
server.enqueue(new MockResponse().setBody("enqueued in the background"));
}).start();
URLConnection connection = server.url("/").url().openConnection();
InputStream in = connection.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in, UTF_8));
assertThat(reader.readLine()).isEqualTo("enqueued in the background");
}
@Test public void nonHexadecimalChunkSize() throws Exception {
server.enqueue(new MockResponse()
.setBody("G\r\nxxxxxxxxxxxxxxxx\r\n0\r\n\r\n")
.clearHeaders()
.addHeader("Transfer-encoding: chunked"));
URLConnection connection = server.url("/").url().openConnection();
InputStream in = connection.getInputStream();
try {
in.read();
fail();
} catch (IOException expected) {
}
}
@Test public void responseTimeout() throws Exception {
server.enqueue(new MockResponse()
.setBody("ABC")
.clearHeaders()
.addHeader("Content-Length: 4"));
server.enqueue(new MockResponse().setBody("DEF"));
URLConnection urlConnection = server.url("/").url().openConnection();
urlConnection.setReadTimeout(1000);
InputStream in = urlConnection.getInputStream();
assertThat(in.read()).isEqualTo('A');
assertThat(in.read()).isEqualTo('B');
assertThat(in.read()).isEqualTo('C');
try {
in.read(); // if Content-Length was accurate, this would return -1 immediately
fail();
} catch (SocketTimeoutException expected) {
}
URLConnection urlConnection2 = server.url("/").url().openConnection();
InputStream in2 = urlConnection2.getInputStream();
assertThat(in2.read()).isEqualTo('D');
assertThat(in2.read()).isEqualTo('E');
assertThat(in2.read()).isEqualTo('F');
assertThat(in2.read()).isEqualTo(-1);
assertThat(server.takeRequest().getSequenceNumber()).isEqualTo(0);
assertThat(server.takeRequest().getSequenceNumber()).isEqualTo(0);
}
@Disabled("Not actually failing where expected")
@Test public void disconnectAtStart() throws Exception {
server.enqueue(new MockResponse()
.setSocketPolicy(SocketPolicy.DISCONNECT_AT_START));
server.enqueue(new MockResponse()); // The jdk's HttpUrlConnection is a bastard.
server.enqueue(new MockResponse());
try {
server.url("/a").url().openConnection().getInputStream();
fail();
} catch (IOException expected) {
}
server.url("/b").url().openConnection().getInputStream(); // Should succeed.
}
/**
* Throttle the request body by sleeping 500ms after every 3 bytes. With a 6-byte request, this
* should yield one sleep for a total delay of 500ms.
*/
@Test public void throttleRequest() throws Exception {
TestUtil.assumeNotWindows();
server.enqueue(new MockResponse()
.throttleBody(3, 500, TimeUnit.MILLISECONDS));
long startNanos = System.nanoTime();
URLConnection connection = server.url("/").url().openConnection();
connection.setDoOutput(true);
connection.getOutputStream().write("ABCDEF".getBytes(UTF_8));
InputStream in = connection.getInputStream();
assertThat(in.read()).isEqualTo(-1);
long elapsedNanos = System.nanoTime() - startNanos;
long elapsedMillis = NANOSECONDS.toMillis(elapsedNanos);
assertThat(elapsedMillis).isBetween(500L, 1000L);
}
/**
* Throttle the response body by sleeping 500ms after every 3 bytes. With a 6-byte response, this
* should yield one sleep for a total delay of 500ms.
*/
@Test public void throttleResponse() throws Exception {
TestUtil.assumeNotWindows();
server.enqueue(new MockResponse()
.setBody("ABCDEF")
.throttleBody(3, 500, TimeUnit.MILLISECONDS));
long startNanos = System.nanoTime();
URLConnection connection = server.url("/").url().openConnection();
InputStream in = connection.getInputStream();
assertThat(in.read()).isEqualTo('A');
assertThat(in.read()).isEqualTo('B');
assertThat(in.read()).isEqualTo('C');
assertThat(in.read()).isEqualTo('D');
assertThat(in.read()).isEqualTo('E');
assertThat(in.read()).isEqualTo('F');
assertThat(in.read()).isEqualTo(-1);
long elapsedNanos = System.nanoTime() - startNanos;
long elapsedMillis = NANOSECONDS.toMillis(elapsedNanos);
assertThat(elapsedMillis).isBetween(500L, 1000L);
}
/** Delay the response body by sleeping 1s. */
@Test public void delayResponse() throws IOException {
TestUtil.assumeNotWindows();
server.enqueue(new MockResponse()
.setBody("ABCDEF")
.setBodyDelay(1, SECONDS));
long startNanos = System.nanoTime();
URLConnection connection = server.url("/").url().openConnection();
InputStream in = connection.getInputStream();
assertThat(in.read()).isEqualTo('A');
long elapsedNanos = System.nanoTime() - startNanos;
long elapsedMillis = NANOSECONDS.toMillis(elapsedNanos);
assertThat(elapsedMillis).isGreaterThanOrEqualTo(1000L);
in.close();
}
@Test public void disconnectRequestHalfway() throws Exception {
server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_DURING_REQUEST_BODY));
// Limit the size of the request body that the server holds in memory to an arbitrary
// 3.5 MBytes so this test can pass on devices with little memory.
server.setBodyLimit(7 * 512 * 1024);
HttpURLConnection connection = (HttpURLConnection) server.url("/").url().openConnection();
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setFixedLengthStreamingMode(1024 * 1024 * 1024); // 1 GB
connection.connect();
OutputStream out = connection.getOutputStream();
byte[] data = new byte[1024 * 1024];
int i;
for (i = 0; i < 1024; i++) {
try {
out.write(data);
out.flush();
if (i == 513) {
// pause slightly after half way to make result more predictable
Thread.sleep(100);
}
} catch (IOException e) {
break;
}
}
// Halfway +/- 0.5%
assertThat((float) i).isCloseTo(512f, offset(5f));
}
@Test public void disconnectResponseHalfway() throws IOException {
server.enqueue(new MockResponse()
.setBody("ab")
.setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY));
URLConnection connection = server.url("/").url().openConnection();
assertThat(connection.getContentLength()).isEqualTo(2);
InputStream in = connection.getInputStream();
assertThat(in.read()).isEqualTo('a');
try {
int byteRead = in.read();
// OpenJDK behavior: end of stream.
assertThat(byteRead).isEqualTo(-1);
} catch (ProtocolException e) {
// On Android, HttpURLConnection is implemented by OkHttp v2. OkHttp
// treats an incomplete response body as a ProtocolException.
}
}
private List<String> headersToList(MockResponse response) {
Headers headers = response.getHeaders();
int size = headers.size();
List<String> headerList = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
headerList.add(headers.name(i) + ": " + headers.value(i));
}
return headerList;
}
@Test public void shutdownWithoutStart() throws IOException {
MockWebServer server = new MockWebServer();
server.shutdown();
}
@Test public void closeViaClosable() throws IOException {
Closeable server = new MockWebServer();
server.close();
}
@Test public void shutdownWithoutEnqueue() throws IOException {
MockWebServer server = new MockWebServer();
server.start();
server.shutdown();
}
@Test public void portImplicitlyStarts() {
assertThat(server.getPort()).isGreaterThan(0);
}
@Test public void hostnameImplicitlyStarts() {
assertThat(server.getHostName()).isNotNull();
}
@Test public void toProxyAddressImplicitlyStarts() {
assertThat(server.toProxyAddress()).isNotNull();
}
@Test public void differentInstancesGetDifferentPorts() throws IOException {
MockWebServer other = new MockWebServer();
assertThat(other.getPort()).isNotEqualTo(server.getPort());
other.shutdown();
}
@Test public void statementStartsAndStops() throws Throwable {
final AtomicBoolean called = new AtomicBoolean();
Statement statement = server.apply(new Statement() {
@Override public void evaluate() throws Throwable {
called.set(true);
server.url("/").url().openConnection().connect();
}
}, Description.EMPTY);
statement.evaluate();
assertThat(called.get()).isTrue();
try {
server.url("/").url().openConnection().connect();
fail();
} catch (ConnectException expected) {
}
}
@Test public void shutdownWhileBlockedDispatching() throws Exception {
// Enqueue a request that'll cause MockWebServer to hang on QueueDispatcher.dispatch().
HttpURLConnection connection = (HttpURLConnection) server.url("/").url().openConnection();
connection.setReadTimeout(500);
try {
connection.getResponseCode();
fail();
} catch (SocketTimeoutException expected) {
}
// Shutting down the server should unblock the dispatcher.
server.shutdown();
}
@Test public void requestUrlReconstructed() throws Exception {
server.enqueue(new MockResponse().setBody("hello world"));
URL url = server.url("/a/deep/path?key=foo%20bar").url();
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
InputStream in = connection.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in, UTF_8));
assertThat(connection.getResponseCode()).isEqualTo(HttpURLConnection.HTTP_OK);
assertThat(reader.readLine()).isEqualTo("hello world");
RecordedRequest request = server.takeRequest();
assertThat(request.getRequestLine()).isEqualTo(
"GET /a/deep/path?key=foo%20bar HTTP/1.1");
HttpUrl requestUrl = request.getRequestUrl();
assertThat(requestUrl.scheme()).isEqualTo("http");
assertThat(requestUrl.host()).isEqualTo(server.getHostName());
assertThat(requestUrl.port()).isEqualTo(server.getPort());
assertThat(requestUrl.encodedPath()).isEqualTo("/a/deep/path");
assertThat(requestUrl.queryParameter("key")).isEqualTo("foo bar");
}
@Test public void shutdownServerAfterRequest() throws Exception {
server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.SHUTDOWN_SERVER_AFTER_RESPONSE));
URL url = server.url("/").url();
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
assertThat(connection.getResponseCode()).isEqualTo(HttpURLConnection.HTTP_OK);
HttpURLConnection refusedConnection = (HttpURLConnection) url.openConnection();
try {
refusedConnection.getResponseCode();
fail("Second connection should be refused");
} catch (ConnectException e) {
assertThat(e.getMessage()).contains("refused");
}
}
@Test public void http100Continue() throws Exception {
server.enqueue(new MockResponse().setBody("response"));
URL url = server.url("/").url();
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoOutput(true);
connection.setRequestProperty("Expect", "100-Continue");
connection.getOutputStream().write("request".getBytes(UTF_8));
InputStream in = connection.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in, UTF_8));
assertThat(reader.readLine()).isEqualTo("response");
RecordedRequest request = server.takeRequest();
assertThat(request.getBody().readUtf8()).isEqualTo("request");
}
@Test public void testH2PriorKnowledgeServerFallback() {
try {
server.setProtocols(asList(Protocol.H2_PRIOR_KNOWLEDGE, Protocol.HTTP_1_1));
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected.getMessage()).isEqualTo(
("protocols containing h2_prior_knowledge cannot use other protocols: "
+ "[h2_prior_knowledge, http/1.1]"));
}
}
@Test public void testH2PriorKnowledgeServerDuplicates() {
try {
// Treating this use case as user error
server.setProtocols(asList(Protocol.H2_PRIOR_KNOWLEDGE, Protocol.H2_PRIOR_KNOWLEDGE));
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected.getMessage()).isEqualTo(
("protocols containing h2_prior_knowledge cannot use other protocols: "
+ "[h2_prior_knowledge, h2_prior_knowledge]"));
}
}
@Test public void testMockWebServerH2PriorKnowledgeProtocol() {
server.setProtocols(asList(Protocol.H2_PRIOR_KNOWLEDGE));
assertThat(server.protocols().size()).isEqualTo(1);
assertThat(server.protocols().get(0)).isEqualTo(Protocol.H2_PRIOR_KNOWLEDGE);
}
@Test public void https() throws Exception {
HandshakeCertificates handshakeCertificates = platform.localhostHandshakeCertificates();
server.useHttps(handshakeCertificates.sslSocketFactory(), false);
server.enqueue(new MockResponse().setBody("abc"));
HttpUrl url = server.url("/");
HttpsURLConnection connection = (HttpsURLConnection) url.url().openConnection();
connection.setSSLSocketFactory(handshakeCertificates.sslSocketFactory());
connection.setHostnameVerifier(new RecordingHostnameVerifier());
assertThat(connection.getResponseCode()).isEqualTo(HttpURLConnection.HTTP_OK);
BufferedReader reader =
new BufferedReader(new InputStreamReader(connection.getInputStream(), UTF_8));
assertThat(reader.readLine()).isEqualTo("abc");
RecordedRequest request = server.takeRequest();
assertThat(request.getRequestUrl().scheme()).isEqualTo("https");
Handshake handshake = request.getHandshake();
assertThat(handshake.tlsVersion()).isNotNull();
assertThat(handshake.cipherSuite()).isNotNull();
assertThat(handshake.localPrincipal()).isNotNull();
assertThat(handshake.localCertificates().size()).isEqualTo(1);
assertThat(handshake.peerPrincipal()).isNull();
assertThat(handshake.peerCertificates().size()).isEqualTo(0);
}
@Test public void httpsWithClientAuth() throws Exception {
platform.assumeNotBouncyCastle();
platform.assumeNotConscrypt();
HeldCertificate clientCa = new HeldCertificate.Builder()
.certificateAuthority(0)
.build();
HeldCertificate serverCa = new HeldCertificate.Builder()
.certificateAuthority(0)
.build();
HeldCertificate serverCertificate = new HeldCertificate.Builder()
.signedBy(serverCa)
.addSubjectAlternativeName(server.getHostName())
.build();
HandshakeCertificates serverHandshakeCertificates = new HandshakeCertificates.Builder()
.addTrustedCertificate(clientCa.certificate())
.heldCertificate(serverCertificate)
.build();
server.useHttps(serverHandshakeCertificates.sslSocketFactory(), false);
server.enqueue(new MockResponse().setBody("abc"));
server.requestClientAuth();
HeldCertificate clientCertificate = new HeldCertificate.Builder()
.signedBy(clientCa)
.build();
HandshakeCertificates clientHandshakeCertificates = new HandshakeCertificates.Builder()
.addTrustedCertificate(serverCa.certificate())
.heldCertificate(clientCertificate)
.build();
HttpUrl url = server.url("/");
HttpsURLConnection connection = (HttpsURLConnection) url.url().openConnection();
connection.setSSLSocketFactory(clientHandshakeCertificates.sslSocketFactory());
connection.setHostnameVerifier(new RecordingHostnameVerifier());
assertThat(connection.getResponseCode()).isEqualTo(HttpURLConnection.HTTP_OK);
BufferedReader reader =
new BufferedReader(new InputStreamReader(connection.getInputStream(), UTF_8));
assertThat(reader.readLine()).isEqualTo("abc");
RecordedRequest request = server.takeRequest();
assertThat(request.getRequestUrl().scheme()).isEqualTo("https");
Handshake handshake = request.getHandshake();
assertThat(handshake.tlsVersion()).isNotNull();
assertThat(handshake.cipherSuite()).isNotNull();
assertThat(handshake.localPrincipal()).isNotNull();
assertThat(handshake.localCertificates().size()).isEqualTo(1);
assertThat(handshake.peerPrincipal()).isNotNull();
assertThat(handshake.peerCertificates().size()).isEqualTo(1);
}
@Test
public void shutdownTwice() throws IOException {
MockWebServer server2 = new MockWebServer();
server2.start();
server2.shutdown();
try {
server2.start();
fail();
} catch (IllegalStateException expected) {
// expected
}
server2.shutdown();
}
public static String getPlatform() {
return System.getProperty("okhttp.platform", "jdk8");
}
}

View File

@ -0,0 +1,633 @@
/*
* Copyright (C) 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.mockwebserver
import java.io.BufferedReader
import java.io.Closeable
import java.io.IOException
import java.io.InputStreamReader
import java.net.ConnectException
import java.net.HttpURLConnection
import java.net.ProtocolException
import java.net.SocketTimeoutException
import java.nio.charset.StandardCharsets
import java.util.Arrays
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import javax.net.ssl.HttpsURLConnection
import okhttp3.Headers
import okhttp3.Protocol
import okhttp3.RecordingHostnameVerifier
import okhttp3.TestUtil.assumeNotWindows
import okhttp3.testing.PlatformRule
import okhttp3.tls.HandshakeCertificates
import okhttp3.tls.HeldCertificate
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.data.Offset
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.fail
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Timeout
import org.junit.jupiter.api.extension.RegisterExtension
import org.junit.runner.Description
import org.junit.runners.model.Statement
@Suppress("deprecation")
@Timeout(30)
@Tag("Slow")
class MockWebServerTest {
@RegisterExtension
var platform = PlatformRule()
private val server = MockWebServer()
@BeforeEach
fun setUp() {
server.start()
}
@AfterEach
fun tearDown() {
server.shutdown()
}
@Test
fun defaultMockResponse() {
val response = MockResponse()
assertThat(headersToList(response)).containsExactly("Content-Length: 0")
assertThat(response.status).isEqualTo("HTTP/1.1 200 OK")
}
@Test
fun setResponseMockReason() {
val reasons = arrayOf(
"Mock Response",
"Informational",
"OK",
"Redirection",
"Client Error",
"Server Error",
"Mock Response"
)
for (i in 0..599) {
val response = MockResponse().setResponseCode(i)
val expectedReason = reasons[i / 100]
assertThat(response.status).isEqualTo("HTTP/1.1 $i $expectedReason")
assertThat(headersToList(response)).containsExactly("Content-Length: 0")
}
}
@Test
fun setStatusControlsWholeStatusLine() {
val response = MockResponse().setStatus("HTTP/1.1 202 That'll do pig")
assertThat(headersToList(response)).containsExactly("Content-Length: 0")
assertThat(response.status).isEqualTo("HTTP/1.1 202 That'll do pig")
}
@Test
fun setBodyAdjustsHeaders() {
val response = MockResponse().setBody("ABC")
assertThat(headersToList(response)).containsExactly("Content-Length: 3")
assertThat(response.getBody()!!.readUtf8()).isEqualTo("ABC")
}
@Test
fun mockResponseAddHeader() {
val response = MockResponse()
.clearHeaders()
.addHeader("Cookie: s=square")
.addHeader("Cookie", "a=android")
assertThat(headersToList(response))
.containsExactly("Cookie: s=square", "Cookie: a=android")
}
@Test
fun mockResponseSetHeader() {
val response = MockResponse()
.clearHeaders()
.addHeader("Cookie: s=square")
.addHeader("Cookie: a=android")
.addHeader("Cookies: delicious")
response.setHeader("cookie", "r=robot")
assertThat(headersToList(response))
.containsExactly("Cookies: delicious", "cookie: r=robot")
}
@Test
fun mockResponseSetHeaders() {
val response = MockResponse()
.clearHeaders()
.addHeader("Cookie: s=square")
.addHeader("Cookies: delicious")
response.setHeaders(Headers.Builder().add("Cookie", "a=android").build())
assertThat(headersToList(response)).containsExactly("Cookie: a=android")
}
@Test
fun regularResponse() {
server.enqueue(MockResponse().setBody("hello world"))
val url = server.url("/").toUrl()
val connection = url.openConnection() as HttpURLConnection
connection.setRequestProperty("Accept-Language", "en-US")
val inputStream = connection.inputStream
val reader = BufferedReader(InputStreamReader(inputStream, StandardCharsets.UTF_8))
assertThat(connection.getResponseCode()).isEqualTo(HttpURLConnection.HTTP_OK)
assertThat(reader.readLine()).isEqualTo("hello world")
val request = server.takeRequest()
assertThat(request.requestLine).isEqualTo("GET / HTTP/1.1")
assertThat(request.getHeader("Accept-Language")).isEqualTo("en-US")
// Server has no more requests.
assertThat(server.takeRequest(100, TimeUnit.MILLISECONDS)).isNull()
}
@Test
fun redirect() {
server.enqueue(
MockResponse()
.setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
.addHeader("Location: " + server.url("/new-path"))
.setBody("This page has moved!")
)
server.enqueue(MockResponse().setBody("This is the new location!"))
val connection = server.url("/").toUrl().openConnection()
val inputStream = connection.getInputStream()
val reader = BufferedReader(InputStreamReader(inputStream, StandardCharsets.UTF_8))
assertThat(reader.readLine()).isEqualTo("This is the new location!")
val first = server.takeRequest()
assertThat(first.requestLine).isEqualTo("GET / HTTP/1.1")
val redirect = server.takeRequest()
assertThat(redirect.requestLine).isEqualTo("GET /new-path HTTP/1.1")
}
/**
* Test that MockWebServer blocks for a call to enqueue() if a request is made before a mock
* response is ready.
*/
@Test
fun dispatchBlocksWaitingForEnqueue() {
Thread {
try {
Thread.sleep(1000)
} catch (ignored: InterruptedException) {
}
server.enqueue(MockResponse().setBody("enqueued in the background"))
}.start()
val connection = server.url("/").toUrl().openConnection()
val inputStream = connection.getInputStream()
val reader = BufferedReader(InputStreamReader(inputStream, StandardCharsets.UTF_8))
assertThat(reader.readLine()).isEqualTo("enqueued in the background")
}
@Test
fun nonHexadecimalChunkSize() {
server.enqueue(
MockResponse()
.setBody("G\r\nxxxxxxxxxxxxxxxx\r\n0\r\n\r\n")
.clearHeaders()
.addHeader("Transfer-encoding: chunked")
)
val connection = server.url("/").toUrl().openConnection()
val inputStream = connection.getInputStream()
try {
inputStream.read()
fail<Any>()
} catch (expected: IOException) {
}
}
@Test
fun responseTimeout() {
server.enqueue(
MockResponse()
.setBody("ABC")
.clearHeaders()
.addHeader("Content-Length: 4")
)
server.enqueue(MockResponse().setBody("DEF"))
val urlConnection = server.url("/").toUrl().openConnection()
urlConnection.setReadTimeout(1000)
val inputStream = urlConnection.getInputStream()
assertThat(inputStream.read()).isEqualTo('A'.code)
assertThat(inputStream.read()).isEqualTo('B'.code)
assertThat(inputStream.read()).isEqualTo('C'.code)
try {
inputStream.read() // if Content-Length was accurate, this would return -1 immediately
fail<Any>()
} catch (expected: SocketTimeoutException) {
}
val urlConnection2 = server.url("/").toUrl().openConnection()
val in2 = urlConnection2.getInputStream()
assertThat(in2.read()).isEqualTo('D'.code)
assertThat(in2.read()).isEqualTo('E'.code)
assertThat(in2.read()).isEqualTo('F'.code)
assertThat(in2.read()).isEqualTo(-1)
assertThat(server.takeRequest().sequenceNumber).isEqualTo(0)
assertThat(server.takeRequest().sequenceNumber).isEqualTo(0)
}
@Disabled("Not actually failing where expected")
@Test
fun disconnectAtStart() {
server.enqueue(
MockResponse()
.setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)
)
server.enqueue(MockResponse()) // The jdk's HttpUrlConnection is a bastard.
server.enqueue(MockResponse())
try {
server.url("/a").toUrl().openConnection().getInputStream()
fail<Any>()
} catch (expected: IOException) {
}
server.url("/b").toUrl().openConnection().getInputStream() // Should succeed.
}
/**
* Throttle the request body by sleeping 500ms after every 3 bytes. With a 6-byte request, this
* should yield one sleep for a total delay of 500ms.
*/
@Test
fun throttleRequest() {
assumeNotWindows()
server.enqueue(
MockResponse()
.throttleBody(3, 500, TimeUnit.MILLISECONDS)
)
val startNanos = System.nanoTime()
val connection = server.url("/").toUrl().openConnection()
connection.setDoOutput(true)
connection.getOutputStream().write("ABCDEF".toByteArray(StandardCharsets.UTF_8))
val inputStream = connection.getInputStream()
assertThat(inputStream.read()).isEqualTo(-1)
val elapsedNanos = System.nanoTime() - startNanos
val elapsedMillis = TimeUnit.NANOSECONDS.toMillis(elapsedNanos)
assertThat(elapsedMillis).isBetween(500L, 1000L)
}
/**
* Throttle the response body by sleeping 500ms after every 3 bytes. With a 6-byte response, this
* should yield one sleep for a total delay of 500ms.
*/
@Test
fun throttleResponse() {
assumeNotWindows()
server.enqueue(
MockResponse()
.setBody("ABCDEF")
.throttleBody(3, 500, TimeUnit.MILLISECONDS)
)
val startNanos = System.nanoTime()
val connection = server.url("/").toUrl().openConnection()
val inputStream = connection.getInputStream()
assertThat(inputStream.read()).isEqualTo('A'.code)
assertThat(inputStream.read()).isEqualTo('B'.code)
assertThat(inputStream.read()).isEqualTo('C'.code)
assertThat(inputStream.read()).isEqualTo('D'.code)
assertThat(inputStream.read()).isEqualTo('E'.code)
assertThat(inputStream.read()).isEqualTo('F'.code)
assertThat(inputStream.read()).isEqualTo(-1)
val elapsedNanos = System.nanoTime() - startNanos
val elapsedMillis = TimeUnit.NANOSECONDS.toMillis(elapsedNanos)
assertThat(elapsedMillis).isBetween(500L, 1000L)
}
/** Delay the response body by sleeping 1s. */
@Test
fun delayResponse() {
assumeNotWindows()
server.enqueue(
MockResponse()
.setBody("ABCDEF")
.setBodyDelay(1, TimeUnit.SECONDS)
)
val startNanos = System.nanoTime()
val connection = server.url("/").toUrl().openConnection()
val inputStream = connection.getInputStream()
assertThat(inputStream.read()).isEqualTo('A'.code)
val elapsedNanos = System.nanoTime() - startNanos
val elapsedMillis = TimeUnit.NANOSECONDS.toMillis(elapsedNanos)
assertThat(elapsedMillis).isGreaterThanOrEqualTo(1000L)
inputStream.close()
}
@Test
fun disconnectRequestHalfway() {
server.enqueue(MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_DURING_REQUEST_BODY))
// Limit the size of the request body that the server holds in memory to an arbitrary
// 3.5 MBytes so this test can pass on devices with little memory.
server.bodyLimit = 7 * 512 * 1024
val connection = server.url("/").toUrl().openConnection() as HttpURLConnection
connection.setRequestMethod("POST")
connection.setDoOutput(true)
connection.setFixedLengthStreamingMode(1024 * 1024 * 1024) // 1 GB
connection.connect()
val out = connection.outputStream
val data = ByteArray(1024 * 1024)
var i = 0
while (i < 1024) {
try {
out.write(data)
out.flush()
if (i == 513) {
// pause slightly after halfway to make result more predictable
Thread.sleep(100)
}
} catch (e: IOException) {
break
}
i++
}
// Halfway +/- 0.5%
assertThat(i.toFloat()).isCloseTo(512f, Offset.offset(5f))
}
@Test
fun disconnectResponseHalfway() {
server.enqueue(
MockResponse()
.setBody("ab")
.setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY)
)
val connection = server.url("/").toUrl().openConnection()
assertThat(connection.getContentLength()).isEqualTo(2)
val inputStream = connection.getInputStream()
assertThat(inputStream.read()).isEqualTo('a'.code)
try {
val byteRead = inputStream.read()
// OpenJDK behavior: end of stream.
assertThat(byteRead).isEqualTo(-1)
} catch (e: ProtocolException) {
// On Android, HttpURLConnection is implemented by OkHttp v2. OkHttp
// treats an incomplete response body as a ProtocolException.
}
}
private fun headersToList(response: MockResponse): List<String> {
val headers = response.headers
val size = headers.size
val headerList: MutableList<String> = ArrayList(size)
for (i in 0 until size) {
headerList.add(headers.name(i) + ": " + headers.value(i))
}
return headerList
}
@Test
fun shutdownWithoutStart() {
val server = MockWebServer()
server.shutdown()
}
@Test
fun closeViaClosable() {
val server: Closeable = MockWebServer()
server.close()
}
@Test
fun shutdownWithoutEnqueue() {
val server = MockWebServer()
server.start()
server.shutdown()
}
@Test
fun portImplicitlyStarts() {
assertThat(server.port).isGreaterThan(0)
}
@Test
fun hostnameImplicitlyStarts() {
assertThat(server.hostName).isNotNull()
}
@Test
fun toProxyAddressImplicitlyStarts() {
assertThat(server.toProxyAddress()).isNotNull()
}
@Test
fun differentInstancesGetDifferentPorts() {
val other = MockWebServer()
assertThat(other.port).isNotEqualTo(server.port)
other.shutdown()
}
@Test
fun statementStartsAndStops() {
val called = AtomicBoolean()
val statement = server.apply(object : Statement() {
override fun evaluate() {
called.set(true)
server.url("/").toUrl().openConnection().connect()
}
}, Description.EMPTY)
statement.evaluate()
assertThat(called.get()).isTrue()
try {
server.url("/").toUrl().openConnection().connect()
fail<Any>()
} catch (expected: ConnectException) {
}
}
@Test
fun shutdownWhileBlockedDispatching() {
// Enqueue a request that'll cause MockWebServer to hang on QueueDispatcher.dispatch().
val connection = server.url("/").toUrl().openConnection() as HttpURLConnection
connection.setReadTimeout(500)
try {
connection.getResponseCode()
fail<Any>()
} catch (expected: SocketTimeoutException) {
}
// Shutting down the server should unblock the dispatcher.
server.shutdown()
}
@Test
fun requestUrlReconstructed() {
server.enqueue(MockResponse().setBody("hello world"))
val url = server.url("/a/deep/path?key=foo%20bar").toUrl()
val connection = url.openConnection() as HttpURLConnection
val inputStream = connection.inputStream
val reader = BufferedReader(InputStreamReader(inputStream, StandardCharsets.UTF_8))
assertThat(connection.getResponseCode()).isEqualTo(HttpURLConnection.HTTP_OK)
assertThat(reader.readLine()).isEqualTo("hello world")
val request = server.takeRequest()
assertThat(request.requestLine).isEqualTo(
"GET /a/deep/path?key=foo%20bar HTTP/1.1"
)
val requestUrl = request.requestUrl
assertThat(requestUrl!!.scheme).isEqualTo("http")
assertThat(requestUrl.host).isEqualTo(server.hostName)
assertThat(requestUrl.port).isEqualTo(server.port)
assertThat(requestUrl.encodedPath).isEqualTo("/a/deep/path")
assertThat(requestUrl.queryParameter("key")).isEqualTo("foo bar")
}
@Test
fun shutdownServerAfterRequest() {
server.enqueue(MockResponse().setSocketPolicy(SocketPolicy.SHUTDOWN_SERVER_AFTER_RESPONSE))
val url = server.url("/").toUrl()
val connection = url.openConnection() as HttpURLConnection
assertThat(connection.getResponseCode()).isEqualTo(HttpURLConnection.HTTP_OK)
val refusedConnection = url.openConnection() as HttpURLConnection
try {
refusedConnection.getResponseCode()
fail<Any>("Second connection should be refused")
} catch (e: ConnectException) {
assertThat(e.message).contains("refused")
}
}
@Test
fun http100Continue() {
server.enqueue(MockResponse().setBody("response"))
val url = server.url("/").toUrl()
val connection = url.openConnection() as HttpURLConnection
connection.setDoOutput(true)
connection.setRequestProperty("Expect", "100-Continue")
connection.outputStream.write("request".toByteArray(StandardCharsets.UTF_8))
val inputStream = connection.inputStream
val reader = BufferedReader(InputStreamReader(inputStream, StandardCharsets.UTF_8))
assertThat(reader.readLine()).isEqualTo("response")
val request = server.takeRequest()
assertThat(request.body.readUtf8()).isEqualTo("request")
}
@Test
fun testH2PriorKnowledgeServerFallback() {
try {
server.protocols = Arrays.asList(Protocol.H2_PRIOR_KNOWLEDGE, Protocol.HTTP_1_1)
fail<Any>()
} catch (expected: IllegalArgumentException) {
assertThat(expected.message).isEqualTo(
"protocols containing h2_prior_knowledge cannot use other protocols: "
+ "[h2_prior_knowledge, http/1.1]"
)
}
}
@Test
fun testH2PriorKnowledgeServerDuplicates() {
try {
// Treating this use case as user error
server.protocols = listOf(Protocol.H2_PRIOR_KNOWLEDGE, Protocol.H2_PRIOR_KNOWLEDGE)
fail<Any>()
} catch (expected: IllegalArgumentException) {
assertThat(expected.message).isEqualTo(
"protocols containing h2_prior_knowledge cannot use other protocols: "
+ "[h2_prior_knowledge, h2_prior_knowledge]"
)
}
}
@Test
fun testMockWebServerH2PriorKnowledgeProtocol() {
server.protocols = Arrays.asList(Protocol.H2_PRIOR_KNOWLEDGE)
assertThat(server.protocols.size).isEqualTo(1)
assertThat(server.protocols[0]).isEqualTo(Protocol.H2_PRIOR_KNOWLEDGE)
}
@Test
fun https() {
val handshakeCertificates = platform.localhostHandshakeCertificates()
server.useHttps(handshakeCertificates.sslSocketFactory(), false)
server.enqueue(MockResponse().setBody("abc"))
val url = server.url("/")
val connection = url.toUrl().openConnection() as HttpsURLConnection
connection.setSSLSocketFactory(handshakeCertificates.sslSocketFactory())
connection.setHostnameVerifier(RecordingHostnameVerifier())
assertThat(connection.getResponseCode()).isEqualTo(HttpURLConnection.HTTP_OK)
val reader = BufferedReader(InputStreamReader(connection.inputStream, StandardCharsets.UTF_8))
assertThat(reader.readLine()).isEqualTo("abc")
val request = server.takeRequest()
assertThat(request.requestUrl!!.scheme).isEqualTo("https")
val handshake = request.handshake
assertThat(handshake!!.tlsVersion).isNotNull()
assertThat(handshake.cipherSuite).isNotNull()
assertThat(handshake.localPrincipal).isNotNull()
assertThat(handshake.localCertificates.size).isEqualTo(1)
assertThat(handshake.peerPrincipal).isNull()
assertThat(handshake.peerCertificates.size).isEqualTo(0)
}
@Test
fun httpsWithClientAuth() {
platform.assumeNotBouncyCastle()
platform.assumeNotConscrypt()
val clientCa = HeldCertificate.Builder()
.certificateAuthority(0)
.build()
val serverCa = HeldCertificate.Builder()
.certificateAuthority(0)
.build()
val serverCertificate = HeldCertificate.Builder()
.signedBy(serverCa)
.addSubjectAlternativeName(server.hostName)
.build()
val serverHandshakeCertificates = HandshakeCertificates.Builder()
.addTrustedCertificate(clientCa.certificate)
.heldCertificate(serverCertificate)
.build()
server.useHttps(serverHandshakeCertificates.sslSocketFactory(), false)
server.enqueue(MockResponse().setBody("abc"))
server.requestClientAuth()
val clientCertificate = HeldCertificate.Builder()
.signedBy(clientCa)
.build()
val clientHandshakeCertificates = HandshakeCertificates.Builder()
.addTrustedCertificate(serverCa.certificate)
.heldCertificate(clientCertificate)
.build()
val url = server.url("/")
val connection = url.toUrl().openConnection() as HttpsURLConnection
connection.setSSLSocketFactory(clientHandshakeCertificates.sslSocketFactory())
connection.setHostnameVerifier(RecordingHostnameVerifier())
assertThat(connection.getResponseCode()).isEqualTo(HttpURLConnection.HTTP_OK)
val reader =
BufferedReader(InputStreamReader(connection.inputStream, StandardCharsets.UTF_8))
assertThat(reader.readLine()).isEqualTo("abc")
val request = server.takeRequest()
assertThat(request.requestUrl!!.scheme).isEqualTo("https")
val handshake = request.handshake
assertThat(handshake!!.tlsVersion).isNotNull()
assertThat(handshake.cipherSuite).isNotNull()
assertThat(handshake.localPrincipal).isNotNull()
assertThat(handshake.localCertificates.size).isEqualTo(1)
assertThat(handshake.peerPrincipal).isNotNull()
assertThat(handshake.peerCertificates.size).isEqualTo(1)
}
@Test
fun shutdownTwice() {
val server2 = MockWebServer()
server2.start()
server2.shutdown()
try {
server2.start()
fail<Any>()
} catch (expected: IllegalStateException) {
// expected
}
server2.shutdown()
}
}

View File

@ -29,7 +29,7 @@ class MockWebServerRuleTest {
val rule = MockWebServerRule()
val called = AtomicBoolean()
val statement: Statement = rule.apply(object : Statement() {
@Throws(Throwable::class) override fun evaluate() {
override fun evaluate() {
called.set(true)
rule.server.url("/").toUrl().openConnection().connect()
}

View File

@ -1,265 +0,0 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.dnsoverhttps;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import mockwebserver3.MockResponse;
import mockwebserver3.MockWebServer;
import mockwebserver3.RecordedRequest;
import mockwebserver3.junit5.internal.MockWebServerExtension;
import okhttp3.Cache;
import okhttp3.Dns;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Protocol;
import okhttp3.testing.PlatformRule;
import okio.Buffer;
import okio.ByteString;
import okio.FileSystem;
import okio.Path;
import okio.fakefilesystem.FakeFileSystem;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail;
@ExtendWith(MockWebServerExtension.class)
@Tag("Slowish")
public class DnsOverHttpsTest {
@RegisterExtension public final PlatformRule platform = new PlatformRule();
private MockWebServer server;
private Dns dns;
private final FileSystem cacheFs = new FakeFileSystem();
private final OkHttpClient bootstrapClient = new OkHttpClient.Builder()
.protocols(asList(Protocol.HTTP_2, Protocol.HTTP_1_1))
.build();
@BeforeEach public void setUp(MockWebServer server) {
this.server = server;
server.setProtocols(bootstrapClient.protocols());
dns = buildLocalhost(bootstrapClient, false);
}
@Test public void getOne() throws Exception {
server.enqueue(dnsResponse(
"0000818000010003000000000567726170680866616365626f6f6b03636f6d0000010001c00c00050001"
+ "00000a6d000603617069c012c0300005000100000cde000c04737461720463313072c012c04200010"
+ "0010000003b00049df00112"));
List<InetAddress> result = dns.lookup("google.com");
assertThat(result).isEqualTo(singletonList(address("157.240.1.18")));
RecordedRequest recordedRequest = server.takeRequest();
assertThat(recordedRequest.getMethod()).isEqualTo("GET");
assertThat(recordedRequest.getPath()).isEqualTo(
"/lookup?ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ");
}
@Test public void getIpv6() throws Exception {
server.enqueue(dnsResponse(
"0000818000010003000000000567726170680866616365626f6f6b03636f6d0000010001c00c00050001"
+ "00000a6d000603617069c012c0300005000100000cde000c04737461720463313072c012c04200010"
+ "0010000003b00049df00112"));
server.enqueue(dnsResponse(
"0000818000010003000000000567726170680866616365626f6f6b03636f6d00001c0001c00c00050001"
+ "00000a1b000603617069c012c0300005000100000b1f000c04737461720463313072c012c042001c0"
+ "0010000003b00102a032880f0290011faceb00c00000002"));
dns = buildLocalhost(bootstrapClient, true);
List<InetAddress> result = dns.lookup("google.com");
assertThat(result.size()).isEqualTo(2);
assertThat(result).contains(address("157.240.1.18"));
assertThat(result).contains(address("2a03:2880:f029:11:face:b00c:0:2"));
RecordedRequest request1 = server.takeRequest();
assertThat(request1.getMethod()).isEqualTo("GET");
RecordedRequest request2 = server.takeRequest();
assertThat(request2.getMethod()).isEqualTo("GET");
assertThat(asList(request1.getPath(), request2.getPath())).containsExactlyInAnyOrder(
"/lookup?ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ",
"/lookup?ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AABwAAQ");
}
@Test public void failure() throws Exception {
server.enqueue(dnsResponse(
"0000818300010000000100000e7364666c6b686673646c6b6a64660265650000010001c01b00060001"
+ "000007070038026e7303746c64c01b0a686f73746d61737465720d6565737469696e7465726e657"
+ "4c01b5adb12c100000e10000003840012750000000e10"));
try {
dns.lookup("google.com");
fail();
} catch (UnknownHostException uhe) {
uhe.printStackTrace();
assertThat(uhe.getMessage()).isEqualTo("google.com: NXDOMAIN");
}
RecordedRequest recordedRequest = server.takeRequest();
assertThat(recordedRequest.getMethod()).isEqualTo("GET");
assertThat(recordedRequest.getPath()).isEqualTo(
"/lookup?ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ");
}
@Test public void failOnExcessiveResponse() {
char[] array = new char[128 * 1024 + 2];
Arrays.fill(array, '0');
server.enqueue(dnsResponse(new String(array)));
try {
dns.lookup("google.com");
fail();
} catch (IOException ioe) {
assertThat(ioe.getMessage()).isEqualTo("google.com");
Throwable cause = ioe.getCause();
assertThat(cause).isInstanceOf(IOException.class);
assertThat(cause).hasMessage("response size exceeds limit (65536 bytes): 65537 bytes");
}
}
@Test public void failOnBadResponse() {
server.enqueue(dnsResponse("00"));
try {
dns.lookup("google.com");
fail();
} catch (IOException ioe) {
assertThat(ioe).hasMessage("google.com");
assertThat(ioe.getCause()).isInstanceOf(EOFException.class);
}
}
// TODO GET preferred order - with tests to confirm this
// 1. successful fresh cached GET response
// 2. unsuccessful (404, 500) fresh cached GET response
// 3. successful network response
// 4. successful stale cached GET response
// 5. unsuccessful response
// TODO how closely to follow POST rules on caching?
@Test public void usesCache() throws Exception {
Cache cache = new Cache(Path.get("cache"), 100 * 1024, cacheFs);
OkHttpClient cachedClient = bootstrapClient.newBuilder().cache(cache).build();
DnsOverHttps cachedDns = buildLocalhost(cachedClient, false);
server.enqueue(dnsResponse(
"0000818000010003000000000567726170680866616365626f6f6b03636f6d0000010001c00c00050001"
+ "00000a6d000603617069c012c0300005000100000cde000c04737461720463313072c012c04200010"
+ "0010000003b00049df00112")
.newBuilder()
.setHeader("cache-control", "private, max-age=298")
.build());
List<InetAddress> result = cachedDns.lookup("google.com");
assertThat(result).containsExactly(address("157.240.1.18"));
RecordedRequest recordedRequest = server.takeRequest();
assertThat(recordedRequest.getMethod()).isEqualTo("GET");
assertThat(recordedRequest.getPath()).isEqualTo(
"/lookup?ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ");
result = cachedDns.lookup("google.com");
assertThat(result).isEqualTo(singletonList(address("157.240.1.18")));
}
@Test public void usesCacheOnlyIfFresh() throws Exception {
Cache cache = new Cache(new File("./target/DnsOverHttpsTest.cache"), 100 * 1024);
OkHttpClient cachedClient = bootstrapClient.newBuilder().cache(cache).build();
DnsOverHttps cachedDns = buildLocalhost(cachedClient, false);
server.enqueue(dnsResponse(
"0000818000010003000000000567726170680866616365626f6f6b03636f6d0000010001c00c00050001"
+ "00000a6d000603617069c012c0300005000100000cde000c04737461720463313072c012c04200010"
+ "0010000003b00049df00112")
.newBuilder()
.setHeader("cache-control", "max-age=1")
.build());
List<InetAddress> result = cachedDns.lookup("google.com");
assertThat(result).containsExactly(address("157.240.1.18"));
RecordedRequest recordedRequest = server.takeRequest(0, TimeUnit.SECONDS);
assertThat(recordedRequest.getMethod()).isEqualTo("GET");
assertThat(recordedRequest.getPath()).isEqualTo(
"/lookup?ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ");
Thread.sleep(2000);
server.enqueue(dnsResponse(
"0000818000010003000000000567726170680866616365626f6f6b03636f6d0000010001c00c00050001"
+ "00000a6d000603617069c012c0300005000100000cde000c04737461720463313072c012c04200010"
+ "0010000003b00049df00112")
.newBuilder()
.setHeader("cache-control", "max-age=1")
.build());
result = cachedDns.lookup("google.com");
assertThat(result).isEqualTo(singletonList(address("157.240.1.18")));
recordedRequest = server.takeRequest(0, TimeUnit.SECONDS);
assertThat(recordedRequest.getMethod()).isEqualTo("GET");
assertThat(recordedRequest.getPath()).isEqualTo(
"/lookup?ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ");
}
private MockResponse dnsResponse(String s) {
return new MockResponse.Builder()
.body(new Buffer().write(ByteString.decodeHex(s)))
.addHeader("content-type", "application/dns-message")
.addHeader("content-length", s.length() / 2)
.build();
}
private DnsOverHttps buildLocalhost(OkHttpClient bootstrapClient, boolean includeIPv6) {
HttpUrl url = server.url("/lookup?ct");
return new DnsOverHttps.Builder().client(bootstrapClient)
.includeIPv6(includeIPv6)
.resolvePrivateAddresses(true)
.url(url)
.build();
}
private static InetAddress address(String host) {
try {
return InetAddress.getByName(host);
} catch (UnknownHostException e) {
// impossible for IP addresses
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,254 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.dnsoverhttps
import java.io.EOFException
import java.io.File
import java.io.IOException
import java.net.InetAddress
import java.net.UnknownHostException
import java.util.Arrays
import java.util.concurrent.TimeUnit
import mockwebserver3.MockResponse
import mockwebserver3.MockWebServer
import mockwebserver3.junit5.internal.MockWebServerExtension
import okhttp3.Cache
import okhttp3.Dns
import okhttp3.OkHttpClient
import okhttp3.Protocol
import okhttp3.testing.PlatformRule
import okio.Buffer
import okio.ByteString.Companion.decodeHex
import okio.Path.Companion.toPath
import okio.fakefilesystem.FakeFileSystem
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Assertions.fail
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.api.extension.RegisterExtension
@ExtendWith(MockWebServerExtension::class)
@Tag("Slowish")
class DnsOverHttpsTest {
@RegisterExtension
val platform = PlatformRule()
private lateinit var server: MockWebServer
private lateinit var dns: Dns
private val cacheFs = FakeFileSystem()
private val bootstrapClient = OkHttpClient.Builder()
.protocols(listOf(Protocol.HTTP_2, Protocol.HTTP_1_1))
.build()
@BeforeEach
fun setUp(server: MockWebServer) {
this.server = server
server.protocols = bootstrapClient.protocols
dns = buildLocalhost(bootstrapClient, false)
}
@Test
fun getOne() {
server.enqueue(
dnsResponse(
"0000818000010003000000000567726170680866616365626f6f6b03636f6d0000010001c00c000500010" +
"0000a6d000603617069c012c0300005000100000cde000c04737461720463313072c012c04200010001000" +
"0003b00049df00112"
)
)
val result = dns.lookup("google.com")
assertThat(result).isEqualTo(listOf(address("157.240.1.18")))
val recordedRequest = server.takeRequest()
assertThat(recordedRequest.method).isEqualTo("GET")
assertThat(recordedRequest.path)
.isEqualTo("/lookup?ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ")
}
@Test
fun getIpv6() {
server.enqueue(
dnsResponse(
"0000818000010003000000000567726170680866616365626f6f6b03636f6d0000010001c00c0005000" +
"100000a6d000603617069c012c0300005000100000cde000c04737461720463313072c012c0420001000" +
"10000003b00049df00112"
)
)
server.enqueue(
dnsResponse(
"0000818000010003000000000567726170680866616365626f6f6b03636f6d00001c0001c00c0005000" +
"100000a1b000603617069c012c0300005000100000b1f000c04737461720463313072c012c042001c000" +
"10000003b00102a032880f0290011faceb00c00000002"
)
)
dns = buildLocalhost(bootstrapClient, true)
val result = dns.lookup("google.com")
assertThat(result.size).isEqualTo(2)
assertThat(result).contains(address("157.240.1.18"))
assertThat(result).contains(address("2a03:2880:f029:11:face:b00c:0:2"))
val request1 = server.takeRequest()
assertThat(request1.method).isEqualTo("GET")
val request2 = server.takeRequest()
assertThat(request2.method).isEqualTo("GET")
assertThat(Arrays.asList(request1.path, request2.path))
.containsExactlyInAnyOrder(
"/lookup?ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ",
"/lookup?ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AABwAAQ"
)
}
@Test
fun failure() {
server.enqueue(
dnsResponse(
"0000818300010000000100000e7364666c6b686673646c6b6a64660265650000010001c01b00060001000" +
"007070038026e7303746c64c01b0a686f73746d61737465720d6565737469696e7465726e6574c01b5adb1" +
"2c100000e10000003840012750000000e10"
)
)
try {
dns.lookup("google.com")
fail<Any>()
} catch (uhe: UnknownHostException) {
assertThat(uhe.message).isEqualTo("google.com: NXDOMAIN")
}
val recordedRequest = server.takeRequest()
assertThat(recordedRequest.method).isEqualTo("GET")
assertThat(recordedRequest.path)
.isEqualTo("/lookup?ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ")
}
@Test
fun failOnExcessiveResponse() {
val array = CharArray(128 * 1024 + 2) { '0' }
server.enqueue(dnsResponse(String(array)))
try {
dns.lookup("google.com")
fail<Any>()
} catch (ioe: IOException) {
assertThat(ioe.message).isEqualTo("google.com")
val cause = ioe.cause
assertThat(cause).isInstanceOf(IOException::class.java)
assertThat(cause).hasMessage("response size exceeds limit (65536 bytes): 65537 bytes")
}
}
@Test
fun failOnBadResponse() {
server.enqueue(dnsResponse("00"))
try {
dns.lookup("google.com")
fail<Any>()
} catch (ioe: IOException) {
assertThat(ioe).hasMessage("google.com")
assertThat(ioe.cause).isInstanceOf(EOFException::class.java)
}
}
// TODO GET preferred order - with tests to confirm this
// 1. successful fresh cached GET response
// 2. unsuccessful (404, 500) fresh cached GET response
// 3. successful network response
// 4. successful stale cached GET response
// 5. unsuccessful response
// TODO how closely to follow POST rules on caching?
@Test
fun usesCache() {
val cache = Cache("cache".toPath(), (100 * 1024).toLong(), cacheFs)
val cachedClient = bootstrapClient.newBuilder().cache(cache).build()
val cachedDns = buildLocalhost(cachedClient, false)
server.enqueue(
dnsResponse(
"0000818000010003000000000567726170680866616365626f6f6b03636f6d0000010001c00c000500010" +
"0000a6d000603617069c012c0300005000100000cde000c04737461720463313072c012c04200010001000" +
"0003b00049df00112"
)
.newBuilder()
.setHeader("cache-control", "private, max-age=298")
.build()
)
var result = cachedDns.lookup("google.com")
assertThat(result).containsExactly(address("157.240.1.18"))
val recordedRequest = server.takeRequest()
assertThat(recordedRequest.method).isEqualTo("GET")
assertThat(recordedRequest.path)
.isEqualTo("/lookup?ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ")
result = cachedDns.lookup("google.com")
assertThat(result).isEqualTo(listOf(address("157.240.1.18")))
}
@Test
fun usesCacheOnlyIfFresh() {
val cache = Cache(File("./target/DnsOverHttpsTest.cache"), 100 * 1024L)
val cachedClient = bootstrapClient.newBuilder().cache(cache).build()
val cachedDns = buildLocalhost(cachedClient, false)
server.enqueue(
dnsResponse(
"0000818000010003000000000567726170680866616365626f6f6b03636f6d0000010001c00c000500010" +
"0000a6d000603617069c012c0300005000100000cde000c04737461720463313072c012c04200010001000" +
"0003b00049df00112"
)
.newBuilder()
.setHeader("cache-control", "max-age=1")
.build()
)
var result = cachedDns.lookup("google.com")
assertThat(result).containsExactly(address("157.240.1.18"))
var recordedRequest = server.takeRequest(0, TimeUnit.SECONDS)
assertThat(recordedRequest!!.method).isEqualTo("GET")
assertThat(recordedRequest.path).isEqualTo(
"/lookup?ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ"
)
Thread.sleep(2000)
server.enqueue(
dnsResponse(
"0000818000010003000000000567726170680866616365626f6f6b03636f6d0000010001c00c000500010" +
"0000a6d000603617069c012c0300005000100000cde000c04737461720463313072c012c04200010001000" +
"0003b00049df00112"
)
.newBuilder()
.setHeader("cache-control", "max-age=1")
.build()
)
result = cachedDns.lookup("google.com")
assertThat(result).isEqualTo(listOf(address("157.240.1.18")))
recordedRequest = server.takeRequest(0, TimeUnit.SECONDS)
assertThat(recordedRequest!!.method).isEqualTo("GET")
assertThat(recordedRequest.path)
.isEqualTo("/lookup?ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ")
}
private fun dnsResponse(s: String): MockResponse {
return MockResponse.Builder()
.body(Buffer().write(s.decodeHex()))
.addHeader("content-type", "application/dns-message")
.addHeader("content-length", s.length / 2)
.build()
}
private fun buildLocalhost(bootstrapClient: OkHttpClient, includeIPv6: Boolean): DnsOverHttps {
val url = server.url("/lookup?ct")
return DnsOverHttps.Builder().client(bootstrapClient)
.includeIPv6(includeIPv6)
.resolvePrivateAddresses(true)
.url(url)
.build()
}
companion object {
private fun address(host: String) = InetAddress.getByName(host)
}
}

View File

@ -1,83 +0,0 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.dnsoverhttps;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.List;
import okio.ByteString;
import org.junit.jupiter.api.Test;
import static okhttp3.dnsoverhttps.DnsRecordCodec.TYPE_A;
import static okhttp3.dnsoverhttps.DnsRecordCodec.TYPE_AAAA;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
public class DnsRecordCodecTest {
@Test public void testGoogleDotComEncoding() {
String encoded = encodeQuery("google.com", TYPE_A);
assertThat(encoded).isEqualTo("AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ");
}
private String encodeQuery(String host, int type) {
return DnsRecordCodec.INSTANCE.encodeQuery(host, type).base64Url().replace("=", "");
}
@Test public void testGoogleDotComEncodingWithIPv6() {
String encoded = encodeQuery("google.com", TYPE_AAAA);
assertThat(encoded).isEqualTo("AAABAAABAAAAAAAABmdvb2dsZQNjb20AABwAAQ");
}
@Test public void testGoogleDotComDecodingFromCloudflare() throws Exception {
List<InetAddress> encoded = DnsRecordCodec.INSTANCE.decodeAnswers("test.com",
ByteString.decodeHex("00008180000100010000000006676f6f676c6503636f6d0000010001c00c000100010"
+ "00000430004d83ad54e"));
assertThat(encoded).containsExactly(InetAddress.getByName("216.58.213.78"));
}
@Test public void testGoogleDotComDecodingFromGoogle() throws Exception {
List<InetAddress> decoded = DnsRecordCodec.INSTANCE.decodeAnswers("test.com",
ByteString.decodeHex("0000818000010003000000000567726170680866616365626f6f6b03636f6d0000010"
+ "001c00c0005000100000a6d000603617069c012c0300005000100000cde000c04737461720463313072c"
+ "012c042000100010000003b00049df00112"));
assertThat(decoded).containsExactly(InetAddress.getByName("157.240.1.18"));
}
@Test public void testGoogleDotComDecodingFromGoogleIPv6() throws Exception {
List<InetAddress> decoded = DnsRecordCodec.INSTANCE.decodeAnswers("test.com",
ByteString.decodeHex("0000818000010003000000000567726170680866616365626f6f6b03636f6d00001c0"
+ "001c00c0005000100000a1b000603617069c012c0300005000100000b1f000c04737461720463313072c"
+ "012c042001c00010000003b00102a032880f0290011faceb00c00000002"));
assertThat(decoded).containsExactly(InetAddress.getByName("2a03:2880:f029:11:face:b00c:0:2"));
}
@Test public void testGoogleDotComDecodingNxdomainFailure() throws Exception {
try {
DnsRecordCodec.INSTANCE.decodeAnswers("sdflkhfsdlkjdf.ee", ByteString.decodeHex("000081830001"
+ "0000000100000e7364666c6b686673646c6b6a64660265650000010001c01b00060001000007070038026e"
+ "7303746c64c01b0a686f73746d61737465720d6565737469696e7465726e6574c01b5adb12c100000e1000"
+ "0003840012750000000e10"));
fail("");
} catch (UnknownHostException uhe) {
assertThat(uhe.getMessage()).isEqualTo("sdflkhfsdlkjdf.ee: NXDOMAIN");
}
}
}

View File

@ -0,0 +1,93 @@
/*
* Copyright (C) 2018 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.
*/
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
package okhttp3.dnsoverhttps
import java.net.InetAddress
import java.net.UnknownHostException
import okhttp3.AsyncDns.Companion.TYPE_A
import okhttp3.AsyncDns.Companion.TYPE_AAAA
import okhttp3.dnsoverhttps.DnsRecordCodec.decodeAnswers
import okio.ByteString.Companion.decodeHex
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.fail
import org.junit.jupiter.api.Test
class DnsRecordCodecTest {
@Test
fun testGoogleDotComEncoding() {
val encoded = encodeQuery("google.com", TYPE_A)
assertThat(encoded).isEqualTo("AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ")
}
private fun encodeQuery(host: String, type: Int): String {
return DnsRecordCodec.encodeQuery(host, type).base64Url().replace("=", "")
}
@Test
fun testGoogleDotComEncodingWithIPv6() {
val encoded = encodeQuery("google.com", TYPE_AAAA)
assertThat(encoded).isEqualTo("AAABAAABAAAAAAAABmdvb2dsZQNjb20AABwAAQ")
}
@Test
fun testGoogleDotComDecodingFromCloudflare() {
val encoded = decodeAnswers(
hostname = "test.com",
byteString = ("00008180000100010000000006676f6f676c6503636f6d0000010001c00c0001000100000043" +
"0004d83ad54e").decodeHex()
)
assertThat(encoded).containsExactly(InetAddress.getByName("216.58.213.78"))
}
@Test
fun testGoogleDotComDecodingFromGoogle() {
val decoded = decodeAnswers(
hostname = "test.com",
byteString = ("0000818000010003000000000567726170680866616365626f6f6b03636f6d0000010001c00c" +
"0005000100000a6d000603617069c012c0300005000100000cde000c04737461720463313072c012c0420001" +
"00010000003b00049df00112").decodeHex()
)
assertThat(decoded).containsExactly(InetAddress.getByName("157.240.1.18"))
}
@Test
fun testGoogleDotComDecodingFromGoogleIPv6() {
val decoded = decodeAnswers(
hostname = "test.com",
byteString = ("0000818000010003000000000567726170680866616365626f6f6b03636f6d00001c0001c00c" +
"0005000100000a1b000603617069c012c0300005000100000b1f000c04737461720463313072c012c042001c" +
"00010000003b00102a032880f0290011faceb00c00000002").decodeHex()
)
assertThat(decoded)
.containsExactly(InetAddress.getByName("2a03:2880:f029:11:face:b00c:0:2"))
}
@Test
fun testGoogleDotComDecodingNxdomainFailure() {
try {
decodeAnswers(
hostname = "sdflkhfsdlkjdf.ee",
byteString = ("0000818300010000000100000e7364666c6b686673646c6b6a64660265650000010001c01b" +
"00060001000007070038026e7303746c64c01b0a686f73746d61737465720d6565737469696e7465726e65" +
"74c01b5adb12c100000e10000003840012750000000e10").decodeHex()
)
fail<Any>("")
} catch (uhe: UnknownHostException) {
assertThat(uhe.message).isEqualTo("sdflkhfsdlkjdf.ee: NXDOMAIN")
}
}
}

View File

@ -1,122 +0,0 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.dnsoverhttps;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
/**
* Temporary registry of known DNS over HTTPS providers.
*
* https://github.com/curl/curl/wiki/DNS-over-HTTPS
*/
public class DohProviders {
static DnsOverHttps buildGoogle(OkHttpClient bootstrapClient) {
return new DnsOverHttps.Builder().client(bootstrapClient)
.url(HttpUrl.get("https://dns.google/dns-query"))
.bootstrapDnsHosts(getByIp("8.8.4.4"), getByIp("8.8.8.8"))
.build();
}
static DnsOverHttps buildGooglePost(OkHttpClient bootstrapClient) {
return new DnsOverHttps.Builder().client(bootstrapClient)
.url(HttpUrl.get("https://dns.google/dns-query"))
.bootstrapDnsHosts(getByIp("8.8.4.4"), getByIp("8.8.8.8"))
.post(true)
.build();
}
static DnsOverHttps buildCloudflareIp(OkHttpClient bootstrapClient) {
return new DnsOverHttps.Builder().client(bootstrapClient)
.url(HttpUrl.get("https://1.1.1.1/dns-query"))
.includeIPv6(false)
.build();
}
static DnsOverHttps buildCloudflare(OkHttpClient bootstrapClient) {
return new DnsOverHttps.Builder().client(bootstrapClient)
.url(HttpUrl.get("https://1.1.1.1/dns-query"))
.bootstrapDnsHosts(getByIp("1.1.1.1"), getByIp("1.0.0.1"))
.includeIPv6(false)
.build();
}
static DnsOverHttps buildCloudflarePost(OkHttpClient bootstrapClient) {
return new DnsOverHttps.Builder().client(bootstrapClient)
.url(HttpUrl.get("https://cloudflare-dns.com/dns-query"))
.bootstrapDnsHosts(getByIp("1.1.1.1"), getByIp("1.0.0.1"))
.includeIPv6(false)
.post(true)
.build();
}
static DnsOverHttps buildCleanBrowsing(OkHttpClient bootstrapClient) {
return new DnsOverHttps.Builder().client(bootstrapClient)
.url(HttpUrl.get("https://doh.cleanbrowsing.org/doh/family-filter/"))
.includeIPv6(false)
.build();
}
static DnsOverHttps buildChantra(OkHttpClient bootstrapClient) {
return new DnsOverHttps.Builder().client(bootstrapClient)
.url(HttpUrl.get("https://dns.dnsoverhttps.net/dns-query"))
.includeIPv6(false)
.build();
}
static DnsOverHttps buildCryptoSx(OkHttpClient bootstrapClient) {
return new DnsOverHttps.Builder().client(bootstrapClient)
.url(HttpUrl.get("https://doh.crypto.sx/dns-query"))
.includeIPv6(false)
.build();
}
public static List<DnsOverHttps> providers(OkHttpClient client, boolean http2Only,
boolean workingOnly, boolean getOnly) {
List<DnsOverHttps> result = new ArrayList<>();
result.add(buildGoogle(client));
if (!getOnly) {
result.add(buildGooglePost(client));
}
result.add(buildCloudflare(client));
result.add(buildCloudflareIp(client));
if (!getOnly) {
result.add(buildCloudflarePost(client));
}
if (!workingOnly) {
//result.add(buildCleanBrowsing(client)); // timeouts
result.add(buildCryptoSx(client)); // 521 - server down
}
result.add(buildChantra(client));
return result;
}
private static InetAddress getByIp(String host) {
try {
return InetAddress.getByName(host);
} catch (UnknownHostException e) {
// unlikely
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,130 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.dnsoverhttps
import java.net.InetAddress
import java.net.UnknownHostException
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
/**
* Temporary registry of known DNS over HTTPS providers.
*
* https://github.com/curl/curl/wiki/DNS-over-HTTPS
*/
object DohProviders {
private fun buildGoogle(bootstrapClient: OkHttpClient): DnsOverHttps {
return DnsOverHttps.Builder()
.client(bootstrapClient)
.url("https://dns.google/dns-query".toHttpUrl())
.bootstrapDnsHosts(getByIp("8.8.4.4"), getByIp("8.8.8.8"))
.build()
}
private fun buildGooglePost(bootstrapClient: OkHttpClient): DnsOverHttps {
return DnsOverHttps.Builder()
.client(bootstrapClient)
.url("https://dns.google/dns-query".toHttpUrl())
.bootstrapDnsHosts(getByIp("8.8.4.4"), getByIp("8.8.8.8"))
.post(true)
.build()
}
private fun buildCloudflareIp(bootstrapClient: OkHttpClient): DnsOverHttps {
return DnsOverHttps.Builder()
.client(bootstrapClient)
.url("https://1.1.1.1/dns-query".toHttpUrl())
.includeIPv6(false)
.build()
}
private fun buildCloudflare(bootstrapClient: OkHttpClient): DnsOverHttps {
return DnsOverHttps.Builder()
.client(bootstrapClient)
.url("https://1.1.1.1/dns-query".toHttpUrl())
.bootstrapDnsHosts(getByIp("1.1.1.1"), getByIp("1.0.0.1"))
.includeIPv6(false)
.build()
}
private fun buildCloudflarePost(bootstrapClient: OkHttpClient): DnsOverHttps {
return DnsOverHttps.Builder()
.client(bootstrapClient)
.url("https://cloudflare-dns.com/dns-query".toHttpUrl())
.bootstrapDnsHosts(getByIp("1.1.1.1"), getByIp("1.0.0.1"))
.includeIPv6(false)
.post(true)
.build()
}
fun buildCleanBrowsing(bootstrapClient: OkHttpClient): DnsOverHttps {
return DnsOverHttps.Builder()
.client(bootstrapClient)
.url("https://doh.cleanbrowsing.org/doh/family-filter/".toHttpUrl())
.includeIPv6(false)
.build()
}
private fun buildChantra(bootstrapClient: OkHttpClient): DnsOverHttps {
return DnsOverHttps.Builder()
.client(bootstrapClient)
.url("https://dns.dnsoverhttps.net/dns-query".toHttpUrl())
.includeIPv6(false)
.build()
}
private fun buildCryptoSx(bootstrapClient: OkHttpClient): DnsOverHttps {
return DnsOverHttps.Builder()
.client(bootstrapClient)
.url("https://doh.crypto.sx/dns-query".toHttpUrl())
.includeIPv6(false)
.build()
}
@JvmStatic
fun providers(
client: OkHttpClient,
http2Only: Boolean,
workingOnly: Boolean,
getOnly: Boolean,
): List<DnsOverHttps> {
return buildList {
add(buildGoogle(client))
if (!getOnly) {
add(buildGooglePost(client))
}
add(buildCloudflare(client))
add(buildCloudflareIp(client))
if (!getOnly) {
add(buildCloudflarePost(client))
}
if (!workingOnly) {
// result += buildCleanBrowsing(client); // timeouts
add(buildCryptoSx(client)) // 521 - server down
}
add(buildChantra(client))
}
}
private fun getByIp(host: String): InetAddress {
return try {
InetAddress.getByName(host)
} catch (e: UnknownHostException) {
// unlikely
throw RuntimeException(e)
}
}
}

View File

@ -1,106 +0,0 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.dnsoverhttps;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.Security;
import java.util.Collections;
import java.util.List;
import okhttp3.Cache;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import static java.util.Arrays.asList;
public class TestDohMain {
public static void main(String[] args) throws IOException {
Security.insertProviderAt(new org.conscrypt.OpenSSLProvider(), 1);
OkHttpClient bootstrapClient = new OkHttpClient.Builder().build();
List<String> names = asList("google.com", "graph.facebook.com", "sdflkhfsdlkjdf.ee");
try {
System.out.println("uncached\n********\n");
List<DnsOverHttps> dnsProviders =
DohProviders.providers(bootstrapClient, false, false, false);
runBatch(dnsProviders, names);
Cache dnsCache =
new Cache(new File("./target/TestDohMain.cache." + System.currentTimeMillis()),
10 * 1024 * 1024);
System.out.println("Bad targets\n***********\n");
HttpUrl url = HttpUrl.get("https://dns.cloudflare.com/.not-so-well-known/run-dmc-query");
List<DnsOverHttps> badProviders = Collections.singletonList(
new DnsOverHttps.Builder().client(bootstrapClient).url(url).post(true).build());
runBatch(badProviders, names);
System.out.println("cached first run\n****************\n");
names = asList("google.com", "graph.facebook.com");
bootstrapClient = bootstrapClient.newBuilder().cache(dnsCache).build();
dnsProviders = DohProviders.providers(bootstrapClient, true, true, true);
runBatch(dnsProviders, names);
System.out.println("cached second run\n*****************\n");
dnsProviders = DohProviders.providers(bootstrapClient, true, true, true);
runBatch(dnsProviders, names);
} finally {
bootstrapClient.connectionPool().evictAll();
bootstrapClient.dispatcher().executorService().shutdownNow();
Cache cache = bootstrapClient.cache();
if (cache != null) {
cache.close();
}
}
}
private static void runBatch(List<DnsOverHttps> dnsProviders, List<String> names) {
long time = System.currentTimeMillis();
for (DnsOverHttps dns : dnsProviders) {
System.out.println("Testing " + dns.url());
for (String host : names) {
System.out.print(host + ": ");
System.out.flush();
try {
List<InetAddress> results = dns.lookup(host);
System.out.println(results);
} catch (UnknownHostException uhe) {
Throwable e = uhe;
while (e != null) {
System.out.println(e);
e = e.getCause();
}
}
}
System.out.println();
}
time = System.currentTimeMillis() - time;
System.out.println("Time: " + (((double) time) / 1000) + " seconds\n");
}
}

View File

@ -0,0 +1,103 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.dnsoverhttps
import java.io.File
import java.net.UnknownHostException
import java.security.Security
import okhttp3.Cache
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.dnsoverhttps.DohProviders.providers
import org.conscrypt.OpenSSLProvider
private fun runBatch(dnsProviders: List<DnsOverHttps>, names: List<String>) {
var time = System.currentTimeMillis()
for (dns in dnsProviders) {
println("Testing ${dns.url}")
for (host in names) {
print("$host: ")
System.out.flush()
try {
val results = dns.lookup(host)
println(results)
} catch (uhe: UnknownHostException) {
var e: Throwable? = uhe
while (e != null) {
println(e)
e = e.cause
}
}
}
println()
}
time = System.currentTimeMillis() - time
println("Time: ${time.toDouble() / 1000} seconds\n")
}
fun main() {
Security.insertProviderAt(OpenSSLProvider(), 1)
var bootstrapClient = OkHttpClient()
var names = listOf("google.com", "graph.facebook.com", "sdflkhfsdlkjdf.ee")
try {
println("uncached\n********\n")
var dnsProviders = providers(
client = bootstrapClient,
http2Only = false,
workingOnly = false,
getOnly = false,
)
runBatch(dnsProviders, names)
val dnsCache = Cache(
directory = File("./target/TestDohMain.cache.${System.currentTimeMillis()}"),
maxSize = 10L * 1024 * 1024
)
println("Bad targets\n***********\n")
val url = "https://dns.cloudflare.com/.not-so-well-known/run-dmc-query".toHttpUrl()
val badProviders = listOf(
DnsOverHttps.Builder()
.client(bootstrapClient)
.url(url)
.post(true)
.build()
)
runBatch(badProviders, names)
println("cached first run\n****************\n")
names = listOf("google.com", "graph.facebook.com")
bootstrapClient = bootstrapClient.newBuilder()
.cache(dnsCache)
.build()
dnsProviders = providers(
client = bootstrapClient,
http2Only = true,
workingOnly = true,
getOnly = true,
)
runBatch(dnsProviders, names)
println("cached second run\n*****************\n")
dnsProviders = providers(
client = bootstrapClient,
http2Only = true,
workingOnly = true,
getOnly = true,
)
runBatch(dnsProviders, names)
} finally {
bootstrapClient.connectionPool.evictAll()
bootstrapClient.dispatcher.executorService.shutdownNow()
bootstrapClient.cache?.close()
}
}

View File

@ -5,6 +5,7 @@ plugins {
dependencies {
testImplementation(libs.squareup.okio)
testImplementation(libs.squareup.moshi)
testImplementation(libs.squareup.moshi.kotlin)
testImplementation(projects.okhttp)
testImplementation(projects.okhttpTestingSupport)
testImplementation(projects.mockwebserver)

View File

@ -1,43 +0,0 @@
/*
* 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.
*/
package okhttp3.internal.http2;
import java.util.List;
import okhttp3.SimpleProvider;
import okhttp3.internal.http2.hpackjson.Story;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ArgumentsSource;
import static okhttp3.internal.http2.hpackjson.HpackJsonUtil.storiesForCurrentDraft;
public class HpackDecodeInteropTest extends HpackDecodeTestBase {
@ParameterizedTest
@ArgumentsSource(StoriesTestProvider.class)
public void testGoodDecoderInterop(Story story) throws Exception {
Assumptions.assumeFalse(story == Story.MISSING, "Test stories missing, checkout git submodule");
testDecoder(story);
}
static class StoriesTestProvider extends SimpleProvider {
@NotNull @Override public List<Object> arguments() throws Exception {
return createStories(storiesForCurrentDraft());
}
}
}

View File

@ -0,0 +1,39 @@
/*
* 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.
*/
package okhttp3.internal.http2
import okhttp3.SimpleProvider
import okhttp3.internal.http2.hpackjson.HpackJsonUtil
import okhttp3.internal.http2.hpackjson.Story
import org.junit.jupiter.api.Assumptions.assumeFalse
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ArgumentsSource
class HpackDecodeInteropTest : HpackDecodeTestBase() {
@ParameterizedTest
@ArgumentsSource(StoriesTestProvider::class)
fun testGoodDecoderInterop(story: Story) {
assumeFalse(
story === Story.MISSING,
"Test stories missing, checkout git submodule"
)
testDecoder(story)
}
internal class StoriesTestProvider : SimpleProvider() {
override fun arguments(): List<Any> = createStories(HpackJsonUtil.storiesForCurrentDraft())
}
}

View File

@ -1,75 +0,0 @@
/*
* 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.
*/
package okhttp3.internal.http2;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import okhttp3.internal.http2.hpackjson.Case;
import okhttp3.internal.http2.hpackjson.HpackJsonUtil;
import okhttp3.internal.http2.hpackjson.Story;
import okio.Buffer;
import static okhttp3.internal.http2.hpackjson.Story.MISSING;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests Hpack implementation using https://github.com/http2jp/hpack-test-case/
*/
public class HpackDecodeTestBase {
/**
* Reads all stories in the folders provided, asserts if no story found.
*/
protected static List<Object> createStories(String[] interopTests)
throws Exception {
if (interopTests.length == 0) {
return Collections.singletonList(MISSING);
}
List<Object> result = new ArrayList<>();
for (String interopTestName : interopTests) {
List<Story> stories = HpackJsonUtil.readStories(interopTestName);
result.addAll(stories);
}
return result;
}
private final Buffer bytesIn = new Buffer();
private final Hpack.Reader hpackReader = new Hpack.Reader(bytesIn, 4096);
protected void testDecoder(Story story) throws Exception {
for (Case testCase : story.getCases()) {
bytesIn.write(testCase.getWire());
hpackReader.readHeaders();
assertSetEquals(String.format("seqno=%d", testCase.getSeqno()), testCase.getHeaders(),
hpackReader.getAndResetHeaderList());
}
}
/**
* Checks if {@code expected} and {@code observed} are equal when viewed as a set and headers are
* deduped.
*
* TODO: See if duped headers should be preserved on decode and verify.
*/
private static void assertSetEquals(
String message, List<Header> expected, List<Header> observed) {
assertThat(new LinkedHashSet<>(observed)).overridingErrorMessage(message).isEqualTo(
new LinkedHashSet<>(expected));
}
}

View File

@ -0,0 +1,75 @@
/*
* 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.
*/
package okhttp3.internal.http2
import okhttp3.internal.http2.hpackjson.HpackJsonUtil
import okhttp3.internal.http2.hpackjson.Story
import okio.Buffer
import org.assertj.core.api.Assertions.assertThat
/**
* Tests Hpack implementation using https://github.com/http2jp/hpack-test-case/
*/
open class HpackDecodeTestBase {
private val bytesIn = Buffer()
private val hpackReader = Hpack.Reader(bytesIn, 4096)
protected fun testDecoder(story: Story) {
for (testCase in story.cases) {
val encoded = testCase.wire ?: continue
bytesIn.write(encoded)
hpackReader.readHeaders()
assertSetEquals(
"seqno=$testCase.seqno",
testCase.headersList,
hpackReader.getAndResetHeaderList()
)
}
}
companion object {
/**
* Reads all stories in the folders provided, asserts if no story found.
*/
@JvmStatic
protected fun createStories(interopTests: Array<String>): List<Any> {
if (interopTests.isEmpty()) return listOf<Any>(Story.MISSING)
val result = mutableListOf<Any>()
for (interopTestName in interopTests) {
val stories = HpackJsonUtil.readStories(interopTestName)
result.addAll(stories)
}
return result
}
/**
* Checks if `expected` and `observed` are equal when viewed as a set and headers are
* deduped.
*
* TODO: See if duped headers should be preserved on decode and verify.
*/
private fun assertSetEquals(
message: String,
expected: List<Header>,
observed: List<Header>,
) {
assertThat(LinkedHashSet(observed))
.overridingErrorMessage(message)
.isEqualTo(LinkedHashSet(expected))
}
}
}

View File

@ -1,61 +0,0 @@
/*
* 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.
*/
package okhttp3.internal.http2;
import java.util.List;
import okhttp3.SimpleProvider;
import okhttp3.internal.http2.hpackjson.Case;
import okhttp3.internal.http2.hpackjson.Story;
import okio.Buffer;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ArgumentsSource;
/**
* Tests for round-tripping headers through hpack..
*/
// TODO: update hpack-test-case with the output of our encoder.
// This test will hide complementary bugs in the encoder and decoder,
// We should test that the encoder is producing responses that are
public class HpackRoundTripTest extends HpackDecodeTestBase {
private static final String[] RAW_DATA = {"raw-data"};
static class StoriesTestProvider extends SimpleProvider {
@NotNull @Override public List<Object> arguments() throws Exception {
return createStories(RAW_DATA);
}
}
private final Buffer bytesOut = new Buffer();
private final Hpack.Writer hpackWriter = new Hpack.Writer(bytesOut);
@ParameterizedTest
@ArgumentsSource(StoriesTestProvider.class)
public void testRoundTrip(Story story) throws Exception {
Assumptions.assumeFalse(story == Story.MISSING, "Test stories missing, checkout git submodule");
Story story2 = story.clone();
// Mutate cases in base class.
for (Case caze : story2.getCases()) {
hpackWriter.writeHeaders(caze.getHeaders());
caze.setWire(bytesOut.readByteString());
}
testDecoder(story2);
}
}

View File

@ -0,0 +1,60 @@
/*
* 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.
*/
package okhttp3.internal.http2
import okhttp3.SimpleProvider
import okhttp3.internal.http2.hpackjson.Case
import okhttp3.internal.http2.hpackjson.Story
import okio.Buffer
import org.junit.jupiter.api.Assumptions.assumeFalse
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ArgumentsSource
/**
* Tests for round-tripping headers through hpack.
*/
// TODO: update hpack-test-case with the output of our encoder.
// This test will hide complementary bugs in the encoder and decoder,
// We should test that the encoder is producing responses that are
class HpackRoundTripTest : HpackDecodeTestBase() {
internal class StoriesTestProvider : SimpleProvider() {
override fun arguments(): List<Any> = createStories(RAW_DATA)
}
private val bytesOut = Buffer()
private val hpackWriter = Hpack.Writer(out = bytesOut)
@ParameterizedTest
@ArgumentsSource(StoriesTestProvider::class)
fun testRoundTrip(story: Story) {
assumeFalse(
story === Story.MISSING,
"Test stories missing, checkout git submodule"
)
val newCases = mutableListOf<Case>()
for (case in story.cases) {
hpackWriter.writeHeaders(case.headersList)
newCases += case.copy(wire = bytesOut.readByteString())
}
testDecoder(story.copy(cases = newCases))
}
companion object {
private val RAW_DATA = arrayOf("raw-data")
}
}

View File

@ -1,67 +0,0 @@
/*
* 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.
*/
package okhttp3.internal.http2.hpackjson;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import okhttp3.internal.http2.Header;
import okio.ByteString;
/**
* Representation of an individual case (set of headers and wire format). There are many cases for a
* single story. This class is used reflectively with Moshi to parse stories.
*/
public class Case implements Cloneable {
private int seqno;
private String wire;
private List<Map<String, String>> headers;
public List<Header> getHeaders() {
List<Header> result = new ArrayList<>();
for (Map<String, String> inputHeader : headers) {
Map.Entry<String, String> entry = inputHeader.entrySet().iterator().next();
result.add(new Header(entry.getKey(), entry.getValue()));
}
return result;
}
public ByteString getWire() {
return ByteString.decodeHex(wire);
}
public int getSeqno() {
return seqno;
}
public void setWire(ByteString wire) {
this.wire = wire.hex();
}
@Override
protected Case clone() throws CloneNotSupportedException {
Case result = new Case();
result.seqno = seqno;
result.wire = wire;
result.headers = new ArrayList<>();
for (Map<String, String> header : headers) {
result.headers.add(new LinkedHashMap<>(header));
}
return result;
}
}

View File

@ -0,0 +1,41 @@
/*
* 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.
*/
package okhttp3.internal.http2.hpackjson
import okhttp3.internal.http2.Header
import okio.ByteString
/**
* Representation of an individual case (set of headers and wire format). There are many cases for a
* single story. This class is used reflectively with Moshi to parse stories.
*/
data class Case(
val seqno: Int = 0,
val wire: ByteString? = null,
val headers: List<Map<String, String>>,
) : Cloneable {
val headersList: List<Header>
get() {
val result = mutableListOf<Header>()
for (inputHeader in headers) {
val (key, value) = inputHeader.entries.iterator().next()
result.add(Header(key, value))
}
return result
}
public override fun clone() = Case(seqno, this.wire, headers)
}

View File

@ -1,107 +0,0 @@
/*
* 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.
*/
package okhttp3.internal.http2.hpackjson;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import okio.Okio;
import static java.util.Arrays.asList;
import static okhttp3.internal.http2.hpackjson.Story.MISSING;
/**
* Utilities for reading HPACK tests.
*/
public final class HpackJsonUtil {
/** Earliest draft that is code-compatible with latest. */
private static final int BASE_DRAFT = 9;
private static final String STORY_RESOURCE_FORMAT = "/hpack-test-case/%s/story_%02d.json";
private static final Moshi MOSHI = new Moshi.Builder().build();
private static final JsonAdapter<Story> STORY_JSON_ADAPTER = MOSHI.adapter(Story.class);
private static Story readStory(InputStream jsonResource) throws IOException {
return STORY_JSON_ADAPTER.fromJson(Okio.buffer(Okio.source(jsonResource)));
}
private static Story readStory(File file) throws IOException {
return STORY_JSON_ADAPTER.fromJson(Okio.buffer(Okio.source(file)));
}
/** Iterate through the hpack-test-case resources, only picking stories for the current draft. */
public static String[] storiesForCurrentDraft() throws URISyntaxException {
URL resource = HpackJsonUtil.class.getResource("/hpack-test-case");
if (resource == null) {
return new String[0];
}
File testCaseDirectory = new File(resource.toURI());
List<String> storyNames = new ArrayList<>();
for (File path : testCaseDirectory.listFiles()) {
if (path.isDirectory() && asList(path.list()).contains("story_00.json")) {
try {
Story firstStory = readStory(new File(path, "story_00.json"));
if (firstStory.getDraft() >= BASE_DRAFT) {
storyNames.add(path.getName());
}
} catch (IOException ignored) {
// Skip this path.
}
}
}
return storyNames.toArray(new String[0]);
}
/**
* Reads stories named "story_xx.json" from the folder provided.
*/
public static List<Story> readStories(String testFolderName) throws Exception {
List<Story> result = new ArrayList<>();
int i = 0;
while (true) { // break after last test.
String storyResourceName = String.format(STORY_RESOURCE_FORMAT, testFolderName, i);
InputStream storyInputStream = HpackJsonUtil.class.getResourceAsStream(storyResourceName);
if (storyInputStream == null) {
break;
}
try {
Story story = readStory(storyInputStream);
story.setFileName(storyResourceName);
result.add(story);
i++;
} finally {
storyInputStream.close();
}
}
if (result.isEmpty()) {
// missing files
result.add(MISSING);
}
return result;
}
private HpackJsonUtil() {
} // Utilities only.
}

View File

@ -0,0 +1,112 @@
/*
* 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.
*/
package okhttp3.internal.http2.hpackjson
import com.squareup.moshi.FromJson
import com.squareup.moshi.Moshi
import com.squareup.moshi.ToJson
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import java.io.File
import java.io.IOException
import okio.BufferedSource
import okio.ByteString
import okio.ByteString.Companion.decodeHex
import okio.FileSystem
import okio.Path
import okio.Path.Companion.toOkioPath
import okio.buffer
import okio.source
/**
* Utilities for reading HPACK tests.
*/
object HpackJsonUtil {
@Suppress("unused")
private val MOSHI = Moshi.Builder()
.add(object : Any() {
@ToJson fun byteStringToJson(byteString: ByteString) = byteString.hex()
@FromJson fun byteStringFromJson(json: String) = json.decodeHex()
})
.add(KotlinJsonAdapterFactory())
.build()
private val STORY_JSON_ADAPTER = MOSHI.adapter(Story::class.java)
private val fileSystem = FileSystem.SYSTEM
private fun readStory(source: BufferedSource): Story {
return STORY_JSON_ADAPTER.fromJson(source)!!
}
private fun readStory(file: Path): Story {
fileSystem.read(file) {
return readStory(this)
}
}
/** Iterate through the hpack-test-case resources, only picking stories for the current draft. */
fun storiesForCurrentDraft(): Array<String> {
val resource = HpackJsonUtil::class.java.getResource("/hpack-test-case")
?: return arrayOf()
val testCaseDirectory = File(resource.toURI()).toOkioPath()
val result = mutableListOf<String>()
for (path in fileSystem.list(testCaseDirectory)) {
val story00 = path / "story_00.json"
if (!fileSystem.exists(story00)) continue
try {
readStory(story00)
result.add(path.name)
} catch (ignored: IOException) {
// Skip this path.
}
}
return result.toTypedArray<String>()
}
/**
* Reads stories named "story_xx.json" from the folder provided.
*/
fun readStories(testFolderName: String): List<Story> {
val result = mutableListOf<Story>()
var i = 0
while (true) { // break after last test.
val storyResourceName = String.format(
"/hpack-test-case/%s/story_%02d.json",
testFolderName,
i,
)
val storyInputStream = HpackJsonUtil::class.java.getResourceAsStream(storyResourceName)
?: break
try {
storyInputStream.use {
val story = readStory(storyInputStream.source().buffer())
.copy(fileName = storyResourceName)
result.add(story)
i++
}
} finally {
storyInputStream.close()
}
}
if (result.isEmpty()) {
// missing files
result.add(Story.MISSING)
}
return result
}
}

View File

@ -1,71 +0,0 @@
/*
* 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.
*/
package okhttp3.internal.http2.hpackjson;
import java.util.ArrayList;
import java.util.List;
/**
* Representation of one story, a set of request headers to encode or decode. This class is used
* reflectively with Moshi to parse stories from files.
*/
public class Story implements Cloneable {
public final static Story MISSING = new Story();
static {
MISSING.setFileName("missing");
}
private transient String fileName;
private List<Case> cases;
private int draft;
private String description;
/**
* The filename is only used in the toString representation.
*/
void setFileName(String fileName) {
this.fileName = fileName;
}
public List<Case> getCases() {
return cases;
}
/** We only expect stories that match the draft we've implemented to pass. */
public int getDraft() {
return draft;
}
@Override
public Story clone() throws CloneNotSupportedException {
Story story = new Story();
story.fileName = this.fileName;
story.cases = new ArrayList<>();
for (Case caze : cases) {
story.cases.add(caze.clone());
}
story.draft = draft;
story.description = description;
return story;
}
@Override
public String toString() {
// Used as the test name.
return fileName;
}
}

View File

@ -0,0 +1,35 @@
/*
* 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.
*/
package okhttp3.internal.http2.hpackjson
/**
* Representation of one story, a set of request headers to encode or decode. This class is used
* reflectively with Moshi to parse stories from files.
*/
data class Story(
val description: String? = null,
val cases: List<Case>,
val fileName: String? = null,
) {
// Used as the test name.
override fun toString() = fileName ?: "?"
companion object {
@JvmField
val MISSING = Story(description = "Missing", cases = listOf(), "missing")
}
}

View File

@ -1,283 +0,0 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.logging;
import java.io.IOException;
import java.net.UnknownHostException;
import mockwebserver3.MockResponse;
import mockwebserver3.MockWebServer;
import mockwebserver3.SocketPolicy;
import mockwebserver3.SocketPolicy.FailHandshake;
import mockwebserver3.junit5.internal.MockWebServerExtension;
import okhttp3.Call;
import okhttp3.EventListener;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.OkHttpClientTestRule;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.TestUtil;
import okhttp3.testing.PlatformRule;
import okhttp3.tls.HandshakeCertificates;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension;
import static java.util.Arrays.asList;
import static okhttp3.Protocol.HTTP_1_1;
import static okhttp3.Protocol.HTTP_2;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail;
@ExtendWith(MockWebServerExtension.class)
public final class LoggingEventListenerTest {
private static final MediaType PLAIN = MediaType.get("text/plain");
@RegisterExtension public final PlatformRule platform = new PlatformRule();
@RegisterExtension public final OkHttpClientTestRule clientTestRule = new OkHttpClientTestRule();
private MockWebServer server;
private final HandshakeCertificates handshakeCertificates
= platform.localhostHandshakeCertificates();
private final LogRecorder logRecorder = new LogRecorder();
private final LoggingEventListener.Factory loggingEventListenerFactory =
new LoggingEventListener.Factory(logRecorder);
private OkHttpClient client;
private HttpUrl url;
@BeforeEach
public void setUp(MockWebServer server) {
this.server = server;
this.client = clientTestRule.newClientBuilder()
.eventListenerFactory(loggingEventListenerFactory)
.sslSocketFactory(handshakeCertificates.sslSocketFactory(),
handshakeCertificates.trustManager())
.retryOnConnectionFailure(false)
.build();
url = server.url("/");
}
@Test
public void get() throws Exception {
TestUtil.assumeNotWindows();
server.enqueue(new MockResponse.Builder()
.body("Hello!")
.setHeader("Content-Type", PLAIN)
.build());
Response response = client.newCall(request().build()).execute();
assertThat(response.body()).isNotNull();
response.body().bytes();
logRecorder
.assertLogMatch("callStart: Request\\{method=GET, url=" + url + "\\}")
.assertLogMatch("proxySelectStart: " + url)
.assertLogMatch("proxySelectEnd: \\[DIRECT\\]")
.assertLogMatch("dnsStart: " + url.host())
.assertLogMatch("dnsEnd: \\[.+\\]")
.assertLogMatch("connectStart: " + url.host() + "/.+ DIRECT")
.assertLogMatch("connectEnd: http/1.1")
.assertLogMatch(
"connectionAcquired: Connection\\{"
+ url.host()
+ ":\\d+, proxy=DIRECT hostAddress="
+ url.host()
+ "/.+ cipherSuite=none protocol=http/1\\.1\\}")
.assertLogMatch("requestHeadersStart")
.assertLogMatch("requestHeadersEnd")
.assertLogMatch("responseHeadersStart")
.assertLogMatch(
"responseHeadersEnd: Response\\{protocol=http/1\\.1, code=200, message=OK, url="
+ url
+ "\\}")
.assertLogMatch("responseBodyStart")
.assertLogMatch("responseBodyEnd: byteCount=6")
.assertLogMatch("connectionReleased")
.assertLogMatch("callEnd")
.assertNoMoreLogs();
}
@Test
public void post() throws IOException {
TestUtil.assumeNotWindows();
server.enqueue(new MockResponse());
client.newCall(request().post(RequestBody.create("Hello!", PLAIN)).build()).execute();
logRecorder
.assertLogMatch("callStart: Request\\{method=POST, url=" + url + "\\}")
.assertLogMatch("proxySelectStart: " + url)
.assertLogMatch("proxySelectEnd: \\[DIRECT\\]")
.assertLogMatch("dnsStart: " + url.host())
.assertLogMatch("dnsEnd: \\[.+\\]")
.assertLogMatch("connectStart: " + url.host() + "/.+ DIRECT")
.assertLogMatch("connectEnd: http/1.1")
.assertLogMatch(
"connectionAcquired: Connection\\{"
+ url.host()
+ ":\\d+, proxy=DIRECT hostAddress="
+ url.host()
+ "/.+ cipherSuite=none protocol=http/1\\.1\\}")
.assertLogMatch("requestHeadersStart")
.assertLogMatch("requestHeadersEnd")
.assertLogMatch("requestBodyStart")
.assertLogMatch("requestBodyEnd: byteCount=6")
.assertLogMatch("responseHeadersStart")
.assertLogMatch(
"responseHeadersEnd: Response\\{protocol=http/1\\.1, code=200, message=OK, url="
+ url
+ "\\}")
.assertLogMatch("responseBodyStart")
.assertLogMatch("responseBodyEnd: byteCount=0")
.assertLogMatch("connectionReleased")
.assertLogMatch("callEnd")
.assertNoMoreLogs();
}
@Test
public void secureGet() throws Exception {
TestUtil.assumeNotWindows();
server.useHttps(handshakeCertificates.sslSocketFactory());
url = server.url("/");
server.enqueue(new MockResponse());
Response response = client.newCall(request().build()).execute();
assertThat(response.body()).isNotNull();
response.body().bytes();
platform.assumeHttp2Support();
logRecorder
.assertLogMatch("callStart: Request\\{method=GET, url=" + url + "\\}")
.assertLogMatch("proxySelectStart: " + url)
.assertLogMatch("proxySelectEnd: \\[DIRECT\\]")
.assertLogMatch("dnsStart: " + url.host())
.assertLogMatch("dnsEnd: \\[.+\\]")
.assertLogMatch("connectStart: " + url.host() + "/.+ DIRECT")
.assertLogMatch("secureConnectStart")
.assertLogMatch("secureConnectEnd: Handshake\\{"
+ "tlsVersion=TLS_1_[23] "
+ "cipherSuite=TLS_.* "
+ "peerCertificates=\\[CN=localhost\\] "
+ "localCertificates=\\[\\]}")
.assertLogMatch("connectEnd: h2")
.assertLogMatch(
"connectionAcquired: Connection\\{"
+ url.host()
+ ":\\d+, proxy=DIRECT hostAddress="
+ url.host()
+ "/.+ cipherSuite=.+ protocol=h2\\}")
.assertLogMatch("requestHeadersStart")
.assertLogMatch("requestHeadersEnd")
.assertLogMatch("responseHeadersStart")
.assertLogMatch(
"responseHeadersEnd: Response\\{protocol=h2, code=200, message=, url=" + url + "\\}")
.assertLogMatch("responseBodyStart")
.assertLogMatch("responseBodyEnd: byteCount=0")
.assertLogMatch("connectionReleased")
.assertLogMatch("callEnd")
.assertNoMoreLogs();
}
@Test
public void dnsFail() throws IOException {
client = new OkHttpClient.Builder()
.dns(hostname -> { throw new UnknownHostException("reason"); })
.eventListenerFactory(loggingEventListenerFactory)
.build();
try {
client.newCall(request().build()).execute();
fail();
} catch (UnknownHostException expected) {
}
logRecorder
.assertLogMatch("callStart: Request\\{method=GET, url=" + url + "\\}")
.assertLogMatch("proxySelectStart: " + url)
.assertLogMatch("proxySelectEnd: \\[DIRECT\\]")
.assertLogMatch("dnsStart: " + url.host())
.assertLogMatch("callFailed: java.net.UnknownHostException: reason")
.assertNoMoreLogs();
}
@Test
public void connectFail() {
TestUtil.assumeNotWindows();
server.useHttps(handshakeCertificates.sslSocketFactory());
server.setProtocols(asList(HTTP_2, HTTP_1_1));
server.enqueue(new MockResponse.Builder()
.socketPolicy(FailHandshake.INSTANCE)
.build());
url = server.url("/");
try {
client.newCall(request().build()).execute();
fail();
} catch (IOException expected) {
}
logRecorder
.assertLogMatch("callStart: Request\\{method=GET, url=" + url + "\\}")
.assertLogMatch("proxySelectStart: " + url)
.assertLogMatch("proxySelectEnd: \\[DIRECT\\]")
.assertLogMatch("dnsStart: " + url.host())
.assertLogMatch("dnsEnd: \\[.+\\]")
.assertLogMatch("connectStart: " + url.host() + "/.+ DIRECT")
.assertLogMatch("secureConnectStart")
.assertLogMatch(
"connectFailed: null \\S+(?:SSLProtocolException|SSLHandshakeException|TlsFatalAlert): (?:Unexpected handshake message: client_hello|Handshake message sequence violation, 1|Read error|Handshake failed|unexpected_message\\(10\\)).*")
.assertLogMatch(
"callFailed: \\S+(?:SSLProtocolException|SSLHandshakeException|TlsFatalAlert): (?:Unexpected handshake message: client_hello|Handshake message sequence violation, 1|Read error|Handshake failed|unexpected_message\\(10\\)).*")
.assertNoMoreLogs();
}
@Test
public void testCacheEvents() {
Request request = new Request.Builder().url(url).build();
Call call = client.newCall(request);
Response response = new Response.Builder().request(request).code(200).message("").protocol(HTTP_2).build();
EventListener listener = loggingEventListenerFactory.create(call);
listener.cacheConditionalHit(call, response);
listener.cacheHit(call, response);
listener.cacheMiss(call);
listener.satisfactionFailure(call, response);
logRecorder
.assertLogMatch("cacheConditionalHit: Response\\{protocol=h2, code=200, message=, url=" + url + "\\}")
.assertLogMatch("cacheHit: Response\\{protocol=h2, code=200, message=, url=" + url + "\\}")
.assertLogMatch("cacheMiss")
.assertLogMatch("satisfactionFailure: Response\\{protocol=h2, code=200, message=, url=" + url + "\\}")
.assertNoMoreLogs();
}
private Request.Builder request() {
return new Request.Builder().url(url);
}
private static class LogRecorder extends HttpLoggingInterceptorTest.LogRecorder {
@Override LogRecorder assertLogMatch(String pattern) {
return (LogRecorder) super.assertLogMatch("\\[\\d+ ms] " + pattern);
}
}
}

View File

@ -0,0 +1,239 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.logging
import java.io.IOException
import java.net.UnknownHostException
import java.util.Arrays
import mockwebserver3.MockResponse
import mockwebserver3.MockWebServer
import mockwebserver3.SocketPolicy.FailHandshake
import mockwebserver3.junit5.internal.MockWebServerExtension
import okhttp3.HttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.OkHttpClientTestRule
import okhttp3.Protocol
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okhttp3.TestUtil.assumeNotWindows
import okhttp3.testing.PlatformRule
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Assertions.fail
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.api.extension.RegisterExtension
@ExtendWith(MockWebServerExtension::class)
class LoggingEventListenerTest {
@RegisterExtension
val platform = PlatformRule()
@RegisterExtension
val clientTestRule = OkHttpClientTestRule()
private lateinit var server: MockWebServer
private val handshakeCertificates = platform.localhostHandshakeCertificates()
private val logRecorder = HttpLoggingInterceptorTest.LogRecorder(
prefix = Regex("""\[\d+ ms] """)
)
private val loggingEventListenerFactory = LoggingEventListener.Factory(logRecorder)
private lateinit var client: OkHttpClient
private lateinit var url: HttpUrl
@BeforeEach
fun setUp(server: MockWebServer) {
this.server = server
client = clientTestRule.newClientBuilder()
.eventListenerFactory(loggingEventListenerFactory)
.sslSocketFactory(
handshakeCertificates.sslSocketFactory(),
handshakeCertificates.trustManager
)
.retryOnConnectionFailure(false)
.build()
url = server.url("/")
}
@Test
fun get() {
assumeNotWindows()
server.enqueue(
MockResponse.Builder()
.body("Hello!")
.setHeader("Content-Type", PLAIN)
.build()
)
val response = client.newCall(request().build()).execute()
assertThat(response.body).isNotNull()
response.body.bytes()
logRecorder
.assertLogMatch(Regex("""callStart: Request\{method=GET, url=$url}"""))
.assertLogMatch(Regex("""proxySelectStart: $url"""))
.assertLogMatch(Regex("""proxySelectEnd: \[DIRECT]"""))
.assertLogMatch(Regex("""dnsStart: ${url.host}"""))
.assertLogMatch(Regex("""dnsEnd: \[.+]"""))
.assertLogMatch(Regex("""connectStart: ${url.host}/.+ DIRECT"""))
.assertLogMatch(Regex("""connectEnd: http/1.1"""))
.assertLogMatch(Regex("""connectionAcquired: Connection\{${url.host}:\d+, proxy=DIRECT hostAddress=${url.host}/.+ cipherSuite=none protocol=http/1\.1}"""))
.assertLogMatch(Regex("""requestHeadersStart"""))
.assertLogMatch(Regex("""requestHeadersEnd"""))
.assertLogMatch(Regex("""responseHeadersStart"""))
.assertLogMatch(Regex("""responseHeadersEnd: Response\{protocol=http/1\.1, code=200, message=OK, url=$url}"""))
.assertLogMatch(Regex("""responseBodyStart"""))
.assertLogMatch(Regex("""responseBodyEnd: byteCount=6"""))
.assertLogMatch(Regex("""connectionReleased"""))
.assertLogMatch(Regex("""callEnd"""))
.assertNoMoreLogs()
}
@Test
fun post() {
assumeNotWindows()
server.enqueue(MockResponse())
client.newCall(request().post("Hello!".toRequestBody(PLAIN)).build()).execute()
logRecorder
.assertLogMatch(Regex("""callStart: Request\{method=POST, url=$url}"""))
.assertLogMatch(Regex("""proxySelectStart: $url"""))
.assertLogMatch(Regex("""proxySelectEnd: \[DIRECT]"""))
.assertLogMatch(Regex("""dnsStart: ${url.host}"""))
.assertLogMatch(Regex("""dnsEnd: \[.+]"""))
.assertLogMatch(Regex("""connectStart: ${url.host}/.+ DIRECT"""))
.assertLogMatch(Regex("""connectEnd: http/1.1"""))
.assertLogMatch(Regex("""connectionAcquired: Connection\{${url.host}:\d+, proxy=DIRECT hostAddress=${url.host}/.+ cipherSuite=none protocol=http/1\.1}"""))
.assertLogMatch(Regex("""requestHeadersStart"""))
.assertLogMatch(Regex("""requestHeadersEnd"""))
.assertLogMatch(Regex("""requestBodyStart"""))
.assertLogMatch(Regex("""requestBodyEnd: byteCount=6"""))
.assertLogMatch(Regex("""responseHeadersStart"""))
.assertLogMatch(Regex("""responseHeadersEnd: Response\{protocol=http/1\.1, code=200, message=OK, url=$url}"""))
.assertLogMatch(Regex("""responseBodyStart"""))
.assertLogMatch(Regex("""responseBodyEnd: byteCount=0"""))
.assertLogMatch(Regex("""connectionReleased"""))
.assertLogMatch(Regex("""callEnd"""))
.assertNoMoreLogs()
}
@Test
fun secureGet() {
assumeNotWindows()
server.useHttps(handshakeCertificates.sslSocketFactory())
url = server.url("/")
server.enqueue(MockResponse())
val response = client.newCall(request().build()).execute()
assertThat(response.body).isNotNull()
response.body.bytes()
platform.assumeHttp2Support()
logRecorder
.assertLogMatch(Regex("""callStart: Request\{method=GET, url=$url}"""))
.assertLogMatch(Regex("""proxySelectStart: $url"""))
.assertLogMatch(Regex("""proxySelectEnd: \[DIRECT]"""))
.assertLogMatch(Regex("""dnsStart: ${url.host}"""))
.assertLogMatch(Regex("""dnsEnd: \[.+]"""))
.assertLogMatch(Regex("""connectStart: ${url.host}/.+ DIRECT"""))
.assertLogMatch(Regex("""secureConnectStart"""))
.assertLogMatch(Regex("""secureConnectEnd: Handshake\{tlsVersion=TLS_1_[23] cipherSuite=TLS_.* peerCertificates=\[CN=localhost] localCertificates=\[]}"""))
.assertLogMatch(Regex("""connectEnd: h2"""))
.assertLogMatch(Regex("""connectionAcquired: Connection\{${url.host}:\d+, proxy=DIRECT hostAddress=${url.host}/.+ cipherSuite=.+ protocol=h2}"""))
.assertLogMatch(Regex("""requestHeadersStart"""))
.assertLogMatch(Regex("""requestHeadersEnd"""))
.assertLogMatch(Regex("""responseHeadersStart"""))
.assertLogMatch(Regex("""responseHeadersEnd: Response\{protocol=h2, code=200, message=, url=$url}"""))
.assertLogMatch(Regex("""responseBodyStart"""))
.assertLogMatch(Regex("""responseBodyEnd: byteCount=0"""))
.assertLogMatch(Regex("""connectionReleased"""))
.assertLogMatch(Regex("""callEnd"""))
.assertNoMoreLogs()
}
@Test
fun dnsFail() {
client = OkHttpClient.Builder()
.dns { _ -> throw UnknownHostException("reason") }
.eventListenerFactory(loggingEventListenerFactory)
.build()
try {
client.newCall(request().build()).execute()
fail<Any>()
} catch (expected: UnknownHostException) {
}
logRecorder
.assertLogMatch(Regex("""callStart: Request\{method=GET, url=$url}"""))
.assertLogMatch(Regex("""proxySelectStart: $url"""))
.assertLogMatch(Regex("""proxySelectEnd: \[DIRECT]"""))
.assertLogMatch(Regex("""dnsStart: ${url.host}"""))
.assertLogMatch(Regex("""callFailed: java.net.UnknownHostException: reason"""))
.assertNoMoreLogs()
}
@Test
fun connectFail() {
assumeNotWindows()
server.useHttps(handshakeCertificates.sslSocketFactory())
server.protocols = Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1)
server.enqueue(
MockResponse.Builder()
.socketPolicy(FailHandshake)
.build()
)
url = server.url("/")
try {
client.newCall(request().build()).execute()
fail<Any>()
} catch (expected: IOException) {
}
logRecorder
.assertLogMatch(Regex("""callStart: Request\{method=GET, url=$url}"""))
.assertLogMatch(Regex("""proxySelectStart: $url"""))
.assertLogMatch(Regex("""proxySelectEnd: \[DIRECT]"""))
.assertLogMatch(Regex("""dnsStart: ${url.host}"""))
.assertLogMatch(Regex("""dnsEnd: \[.+]"""))
.assertLogMatch(Regex("""connectStart: ${url.host}/.+ DIRECT"""))
.assertLogMatch(Regex("""secureConnectStart"""))
.assertLogMatch(Regex("""connectFailed: null \S+(?:SSLProtocolException|SSLHandshakeException|TlsFatalAlert): (?:Unexpected handshake message: client_hello|Handshake message sequence violation, 1|Read error|Handshake failed|unexpected_message\(10\)).*"""))
.assertLogMatch(Regex("""callFailed: \S+(?:SSLProtocolException|SSLHandshakeException|TlsFatalAlert): (?:Unexpected handshake message: client_hello|Handshake message sequence violation, 1|Read error|Handshake failed|unexpected_message\(10\)).*"""))
.assertNoMoreLogs()
}
@Test
fun testCacheEvents() {
val request = Request.Builder().url(url).build()
val call = client.newCall(request)
val response =
Response.Builder().request(request).code(200).message("").protocol(Protocol.HTTP_2)
.build()
val listener = loggingEventListenerFactory.create(call)
listener.cacheConditionalHit(call, response)
listener.cacheHit(call, response)
listener.cacheMiss(call)
listener.satisfactionFailure(call, response)
logRecorder
.assertLogMatch(Regex("""cacheConditionalHit: Response\{protocol=h2, code=200, message=, url=$url}"""))
.assertLogMatch(Regex("""cacheHit: Response\{protocol=h2, code=200, message=, url=$url}"""))
.assertLogMatch(Regex("""cacheMiss"""))
.assertLogMatch(Regex("""satisfactionFailure: Response\{protocol=h2, code=200, message=, url=$url}"""))
.assertNoMoreLogs()
}
private fun request(): Request.Builder {
return Request.Builder().url(url)
}
companion object {
private val PLAIN = "text/plain".toMediaType()
}
}

View File

@ -1,52 +0,0 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.sse.internal;
import java.util.Objects;
import javax.annotation.Nullable;
final class Event {
final @Nullable String id;
final @Nullable String type;
final String data;
Event(@Nullable String id, @Nullable String type, String data) {
if (data == null) throw new NullPointerException("data == null");
this.id = id;
this.type = type;
this.data = data;
}
@Override public String toString() {
return "Event{id='" + id + "', type='" + type + "', data='" + data + "'}";
}
@Override public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Event)) return false;
Event other = (Event) o;
return Objects.equals(id, other.id)
&& Objects.equals(type, other.type)
&& data.equals(other.data);
}
@Override public int hashCode() {
int result = Objects.hashCode(id);
result = 31 * result + Objects.hashCode(type);
result = 31 * result + data.hashCode();
return result;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 Square, Inc.
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -13,15 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.brotli;
package okhttp3.sse.internal
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
public class BrotliInterceptorJavaApiTest {
@Test
@Disabled("api only")
public void testApi() {
BrotliInterceptor.INSTANCE.intercept(null);
}
}
internal data class Event(
val id: String?,
val type: String?,
val data: String,
)

View File

@ -1,236 +0,0 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.sse.internal;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import mockwebserver3.MockResponse;
import mockwebserver3.MockWebServer;
import mockwebserver3.junit5.internal.MockWebServerExtension;
import okhttp3.OkHttpClient;
import okhttp3.OkHttpClientTestRule;
import okhttp3.RecordingEventListener;
import okhttp3.Request;
import okhttp3.sse.EventSource;
import okhttp3.sse.EventSources;
import okhttp3.testing.PlatformRule;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junitpioneer.jupiter.RetryingTest;
import static org.assertj.core.api.Assertions.assertThat;
@Tag("Slowish")
@ExtendWith(MockWebServerExtension.class)
public final class EventSourceHttpTest {
@RegisterExtension public final PlatformRule platform = new PlatformRule();
private MockWebServer server;
@RegisterExtension public final OkHttpClientTestRule clientTestRule = new OkHttpClientTestRule();
private final RecordingEventListener eventListener = new RecordingEventListener();
private final EventSourceRecorder listener = new EventSourceRecorder();
private OkHttpClient client = clientTestRule.newClientBuilder()
.eventListenerFactory(clientTestRule.wrap(eventListener))
.build();
@BeforeEach public void before(MockWebServer server) {
this.server = server;
}
@AfterEach public void after() {
listener.assertExhausted();
}
@Test public void event() {
server.enqueue(new MockResponse.Builder()
.body(""
+ "data: hey\n"
+ "\n").setHeader("content-type", "text/event-stream")
.build());
EventSource source = newEventSource();
assertThat(source.request().url().encodedPath()).isEqualTo("/");
listener.assertOpen();
listener.assertEvent(null, null, "hey");
listener.assertClose();
}
@RetryingTest(5)
public void cancelInEventShortCircuits() throws IOException {
server.enqueue(new MockResponse.Builder()
.body(""
+ "data: hey\n"
+ "\n").setHeader("content-type", "text/event-stream")
.build());
listener.enqueueCancel(); // Will cancel in onOpen().
newEventSource();
listener.assertOpen();
listener.assertFailure("canceled");
}
@Test public void badContentType() {
server.enqueue(new MockResponse.Builder()
.body(""
+ "data: hey\n"
+ "\n").setHeader("content-type", "text/plain")
.build());
newEventSource();
listener.assertFailure("Invalid content-type: text/plain");
}
@Test public void badResponseCode() {
server.enqueue(new MockResponse.Builder()
.body(""
+ "data: hey\n"
+ "\n")
.setHeader("content-type", "text/event-stream")
.code(401)
.build());
newEventSource();
listener.assertFailure(null);
}
@Test public void fullCallTimeoutDoesNotApplyOnceConnected() throws Exception {
client = client.newBuilder()
.callTimeout(250, TimeUnit.MILLISECONDS)
.build();
server.enqueue(new MockResponse.Builder()
.bodyDelay(500, TimeUnit.MILLISECONDS)
.setHeader("content-type", "text/event-stream")
.body("data: hey\n\n")
.build());
EventSource source = newEventSource();
assertThat(source.request().url().encodedPath()).isEqualTo("/");
listener.assertOpen();
listener.assertEvent(null, null, "hey");
listener.assertClose();
}
@Test public void fullCallTimeoutAppliesToSetup() throws Exception {
client = client.newBuilder()
.callTimeout(250, TimeUnit.MILLISECONDS)
.build();
server.enqueue(new MockResponse.Builder()
.headersDelay(500, TimeUnit.MILLISECONDS)
.setHeader("content-type", "text/event-stream")
.body("data: hey\n\n")
.build());
newEventSource();
listener.assertFailure("timeout");
}
@Test public void retainsAccept() throws InterruptedException {
server.enqueue(new MockResponse.Builder()
.body(""
+ "data: hey\n"
+ "\n").setHeader("content-type", "text/event-stream")
.build());
EventSource source = newEventSource("text/plain");
listener.assertOpen();
listener.assertEvent(null, null, "hey");
listener.assertClose();
assertThat(server.takeRequest().getHeaders().get("Accept")).isEqualTo("text/plain");
}
@Test public void setsMissingAccept() throws InterruptedException {
server.enqueue(new MockResponse.Builder()
.body(""
+ "data: hey\n"
+ "\n").setHeader("content-type", "text/event-stream")
.build());
EventSource source = newEventSource();
listener.assertOpen();
listener.assertEvent(null, null, "hey");
listener.assertClose();
assertThat(server.takeRequest().getHeaders().get("Accept")).isEqualTo("text/event-stream");
}
@Test public void eventListenerEvents() {
server.enqueue(new MockResponse.Builder()
.body(""
+ "data: hey\n"
+ "\n").setHeader("content-type", "text/event-stream")
.build());
EventSource source = newEventSource();
assertThat(source.request().url().encodedPath()).isEqualTo("/");
listener.assertOpen();
listener.assertEvent(null, null, "hey");
listener.assertClose();
assertThat(eventListener.recordedEventTypes()).containsExactly(
"CallStart",
"ProxySelectStart",
"ProxySelectEnd",
"DnsStart",
"DnsEnd",
"ConnectStart",
"ConnectEnd",
"ConnectionAcquired",
"RequestHeadersStart",
"RequestHeadersEnd",
"ResponseHeadersStart",
"ResponseHeadersEnd",
"ResponseBodyStart",
"ResponseBodyEnd",
"ConnectionReleased",
"CallEnd"
);
}
private EventSource newEventSource() {
return newEventSource(null);
}
private EventSource newEventSource(@Nullable String accept) {
Request.Builder builder = new Request.Builder()
.url(server.url("/"));
if (accept != null) {
builder.header("Accept", accept);
}
Request request = builder
.build();
EventSource.Factory factory = EventSources.createFactory(client);
return factory.newEventSource(request, listener);
}
}

View File

@ -0,0 +1,262 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.sse.internal
import java.util.concurrent.TimeUnit
import mockwebserver3.MockResponse
import mockwebserver3.MockWebServer
import mockwebserver3.junit5.internal.MockWebServerExtension
import okhttp3.OkHttpClientTestRule
import okhttp3.RecordingEventListener
import okhttp3.Request
import okhttp3.sse.EventSource
import okhttp3.sse.EventSources.createFactory
import okhttp3.testing.PlatformRule
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.api.extension.RegisterExtension
import org.junitpioneer.jupiter.RetryingTest
@Tag("Slowish")
@ExtendWith(MockWebServerExtension::class)
class EventSourceHttpTest {
@RegisterExtension
val platform = PlatformRule()
private lateinit var server: MockWebServer
@RegisterExtension
val clientTestRule = OkHttpClientTestRule()
private val eventListener = RecordingEventListener()
private val listener = EventSourceRecorder()
private var client = clientTestRule.newClientBuilder()
.eventListenerFactory(clientTestRule.wrap(eventListener))
.build()
@BeforeEach
fun before(server: MockWebServer) {
this.server = server
}
@AfterEach
fun after() {
listener.assertExhausted()
}
@Test
fun event() {
server.enqueue(
MockResponse.Builder()
.body(
"""
|data: hey
|
|
""".trimMargin()
).setHeader("content-type", "text/event-stream")
.build()
)
val source = newEventSource()
assertThat(source.request().url.encodedPath).isEqualTo("/")
listener.assertOpen()
listener.assertEvent(null, null, "hey")
listener.assertClose()
}
@RetryingTest(5)
fun cancelInEventShortCircuits() {
server.enqueue(
MockResponse.Builder()
.body(
"""
|data: hey
|
|
""".trimMargin()
).setHeader("content-type", "text/event-stream")
.build()
)
listener.enqueueCancel() // Will cancel in onOpen().
newEventSource()
listener.assertOpen()
listener.assertFailure("canceled")
}
@Test
fun badContentType() {
server.enqueue(
MockResponse.Builder()
.body(
"""
|data: hey
|
|
""".trimMargin()
).setHeader("content-type", "text/plain")
.build()
)
newEventSource()
listener.assertFailure("Invalid content-type: text/plain")
}
@Test
fun badResponseCode() {
server.enqueue(
MockResponse.Builder()
.body(
"""
|data: hey
|
|
""".trimMargin()
)
.setHeader("content-type", "text/event-stream")
.code(401)
.build()
)
newEventSource()
listener.assertFailure(null)
}
@Test
fun fullCallTimeoutDoesNotApplyOnceConnected() {
client = client.newBuilder()
.callTimeout(250, TimeUnit.MILLISECONDS)
.build()
server.enqueue(
MockResponse.Builder()
.bodyDelay(500, TimeUnit.MILLISECONDS)
.setHeader("content-type", "text/event-stream")
.body("data: hey\n\n")
.build()
)
val source = newEventSource()
assertThat(source.request().url.encodedPath).isEqualTo("/")
listener.assertOpen()
listener.assertEvent(null, null, "hey")
listener.assertClose()
}
@Test
fun fullCallTimeoutAppliesToSetup() {
client = client.newBuilder()
.callTimeout(250, TimeUnit.MILLISECONDS)
.build()
server.enqueue(
MockResponse.Builder()
.headersDelay(500, TimeUnit.MILLISECONDS)
.setHeader("content-type", "text/event-stream")
.body("data: hey\n\n")
.build()
)
newEventSource()
listener.assertFailure("timeout")
}
@Test
fun retainsAccept() {
server.enqueue(
MockResponse.Builder()
.body(
"""
|data: hey
|
|
""".trimMargin()
)
.setHeader("content-type", "text/event-stream")
.build()
)
newEventSource("text/plain")
listener.assertOpen()
listener.assertEvent(null, null, "hey")
listener.assertClose()
assertThat(server.takeRequest().headers["Accept"]).isEqualTo("text/plain")
}
@Test
fun setsMissingAccept() {
server.enqueue(
MockResponse.Builder()
.body(
"""
|data: hey
|
|
""".trimMargin()
).setHeader("content-type", "text/event-stream")
.build()
)
newEventSource()
listener.assertOpen()
listener.assertEvent(null, null, "hey")
listener.assertClose()
assertThat(server.takeRequest().headers["Accept"])
.isEqualTo("text/event-stream")
}
@Test
fun eventListenerEvents() {
server.enqueue(
MockResponse.Builder()
.body(
"""
|data: hey
|
|
""".trimMargin()
).setHeader("content-type", "text/event-stream")
.build()
)
val source = newEventSource()
assertThat(source.request().url.encodedPath).isEqualTo("/")
listener.assertOpen()
listener.assertEvent(null, null, "hey")
listener.assertClose()
assertThat(eventListener.recordedEventTypes()).containsExactly(
"CallStart",
"ProxySelectStart",
"ProxySelectEnd",
"DnsStart",
"DnsEnd",
"ConnectStart",
"ConnectEnd",
"ConnectionAcquired",
"RequestHeadersStart",
"RequestHeadersEnd",
"ResponseHeadersStart",
"ResponseHeadersEnd",
"ResponseBodyStart",
"ResponseBodyEnd",
"ConnectionReleased",
"CallEnd"
)
}
private fun newEventSource(accept: String? = null): EventSource {
val builder = Request.Builder()
.url(server.url("/"))
if (accept != null) {
builder.header("Accept", accept)
}
val request = builder.build()
val factory = createFactory(client)
return factory.newEventSource(request, listener)
}
}

View File

@ -1,166 +0,0 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.sse.internal;
import java.io.IOException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
import javax.annotation.Nullable;
import okhttp3.Response;
import okhttp3.internal.platform.Platform;
import okhttp3.sse.EventSource;
import okhttp3.sse.EventSourceListener;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.assertj.core.api.Assertions.assertThat;
public final class EventSourceRecorder extends EventSourceListener {
private final BlockingQueue<Object> events = new LinkedBlockingDeque<>();
private boolean cancel = false;
public void enqueueCancel() {
cancel = true;
}
@Override public void onOpen(EventSource eventSource, Response response) {
Platform.get().log("[ES] onOpen", Platform.INFO, null);
events.add(new Open(eventSource, response));
drainCancelQueue(eventSource);
}
@Override public void onEvent(EventSource eventSource, @Nullable String id, @Nullable String type,
String data) {
Platform.get().log("[ES] onEvent", Platform.INFO, null);
events.add(new Event(id, type, data));
drainCancelQueue(eventSource);
}
@Override public void onClosed(EventSource eventSource) {
Platform.get().log("[ES] onClosed", Platform.INFO, null);
events.add(new Closed());
drainCancelQueue(eventSource);
}
@Override
public void onFailure(EventSource eventSource, @Nullable Throwable t, @Nullable Response response) {
Platform.get().log("[ES] onFailure", Platform.INFO, t);
events.add(new Failure(t, response));
drainCancelQueue(eventSource);
}
private void drainCancelQueue(EventSource eventSource) {
if (cancel) {
cancel = false;
eventSource.cancel();
}
}
private Object nextEvent() {
try {
Object event = events.poll(10, SECONDS);
if (event == null) {
throw new AssertionError("Timed out waiting for event.");
}
return event;
} catch (InterruptedException e) {
throw new AssertionError(e);
}
}
public void assertExhausted() {
assertThat(events).isEmpty();
}
public void assertEvent(@Nullable String id, @Nullable String type, String data) {
Object actual = nextEvent();
assertThat(actual).isEqualTo(new Event(id, type, data));
}
public EventSource assertOpen() {
Object event = nextEvent();
if (!(event instanceof Open)) {
throw new AssertionError("Expected Open but was " + event);
}
return ((Open) event).eventSource;
}
public void assertClose() {
Object event = nextEvent();
if (!(event instanceof Closed)) {
throw new AssertionError("Expected Open but was " + event);
}
}
public void assertFailure(@Nullable String message) {
Object event = nextEvent();
if (!(event instanceof Failure)) {
throw new AssertionError("Expected Failure but was " + event);
}
if (message != null) {
assertThat(((Failure) event).t.getMessage()).isEqualTo(message);
} else {
assertThat(((Failure) event).t).isNull();
}
}
static final class Open {
final EventSource eventSource;
final Response response;
Open(EventSource eventSource, Response response) {
this.eventSource = eventSource;
this.response = response;
}
@Override public String toString() {
return "Open[" + response + ']';
}
}
static final class Failure {
final Throwable t;
final Response response;
final String responseBody;
Failure(Throwable t, Response response) {
this.t = t;
this.response = response;
String responseBody = null;
if (response != null) {
try {
responseBody = response.body().string();
} catch (IOException ignored) {
} catch (IllegalStateException ise) {
// Body was stripped (UnreadableResponseBody)
}
}
this.responseBody = responseBody;
}
@Override public String toString() {
if (response == null) {
return "Failure[" + t + "]";
}
return "Failure[" + response + "]";
}
}
static final class Closed {
@Override public String toString() {
return "Closed[]";
}
}
}

View File

@ -0,0 +1,125 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.sse.internal
import java.util.concurrent.LinkedBlockingDeque
import java.util.concurrent.TimeUnit
import okhttp3.Response
import okhttp3.internal.platform.Platform
import okhttp3.internal.platform.Platform.Companion.get
import okhttp3.sse.EventSource
import okhttp3.sse.EventSourceListener
import org.assertj.core.api.Assertions.assertThat
class EventSourceRecorder : EventSourceListener() {
private val events = LinkedBlockingDeque<Any>()
private var cancel = false
fun enqueueCancel() {
cancel = true
}
override fun onOpen(eventSource: EventSource, response: Response) {
get().log("[ES] onOpen", Platform.INFO, null)
events.add(Open(eventSource, response))
drainCancelQueue(eventSource)
}
override fun onEvent(
eventSource: EventSource,
id: String?,
type: String?,
data: String,
) {
get().log("[ES] onEvent", Platform.INFO, null)
events.add(Event(id, type, data))
drainCancelQueue(eventSource)
}
override fun onClosed(
eventSource: EventSource,
) {
get().log("[ES] onClosed", Platform.INFO, null)
events.add(Closed)
drainCancelQueue(eventSource)
}
override fun onFailure(
eventSource: EventSource,
t: Throwable?,
response: Response?,
) {
get().log("[ES] onFailure", Platform.INFO, t)
events.add(Failure(t, response))
drainCancelQueue(eventSource)
}
private fun drainCancelQueue(
eventSource: EventSource,
) {
if (cancel) {
cancel = false
eventSource.cancel()
}
}
private fun nextEvent(): Any {
return events.poll(10, TimeUnit.SECONDS)
?: throw AssertionError("Timed out waiting for event.")
}
fun assertExhausted() {
assertThat(events).isEmpty()
}
fun assertEvent(
id: String?,
type: String?,
data: String,
) {
assertThat(nextEvent()).isEqualTo(Event(id, type, data))
}
fun assertOpen(): EventSource {
val event = nextEvent() as Open
return event.eventSource
}
fun assertClose() {
nextEvent() as Closed
}
fun assertFailure(message: String?) {
val event = nextEvent() as Failure
if (message != null) {
assertThat(event.t!!.message).isEqualTo(message)
} else {
assertThat(event.t).isNull()
}
}
internal data class Open(
val eventSource: EventSource,
val response: Response,
)
internal data class Failure(
val t: Throwable?,
val response: Response?,
)
internal object Closed
}

View File

@ -1,89 +0,0 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.sse.internal;
import java.io.IOException;
import mockwebserver3.MockResponse;
import mockwebserver3.MockWebServer;
import mockwebserver3.junit5.internal.MockWebServerExtension;
import okhttp3.OkHttpClient;
import okhttp3.OkHttpClientTestRule;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.sse.EventSources;
import okhttp3.testing.PlatformRule;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension;
@Tag("Slowish")
@ExtendWith(MockWebServerExtension.class)
public final class EventSourcesHttpTest {
@RegisterExtension public final PlatformRule platform = new PlatformRule();
private MockWebServer server;
@RegisterExtension public final OkHttpClientTestRule clientTestRule = new OkHttpClientTestRule();
private final EventSourceRecorder listener = new EventSourceRecorder();
private OkHttpClient client = clientTestRule.newClient();
@BeforeEach public void before(MockWebServer server) {
this.server = server;
}
@AfterEach public void after() {
listener.assertExhausted();
}
@Test public void processResponse() throws IOException {
server.enqueue(new MockResponse.Builder()
.body(""
+ "data: hey\n"
+ "\n").setHeader("content-type", "text/event-stream")
.build());
Request request = new Request.Builder()
.url(server.url("/"))
.build();
Response response = client.newCall(request).execute();
EventSources.processResponse(response, listener);
listener.assertOpen();
listener.assertEvent(null, null, "hey");
listener.assertClose();
}
@Test public void cancelShortCircuits() throws IOException {
server.enqueue(new MockResponse.Builder()
.body(""
+ "data: hey\n"
+ "\n").setHeader("content-type", "text/event-stream")
.build());
listener.enqueueCancel(); // Will cancel in onOpen().
Request request = new Request.Builder()
.url(server.url("/"))
.build();
Response response = client.newCall(request).execute();
EventSources.processResponse(response, listener);
listener.assertOpen();
listener.assertFailure("canceled");
}
}

View File

@ -0,0 +1,100 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.sse.internal
import mockwebserver3.MockResponse
import mockwebserver3.MockWebServer
import mockwebserver3.junit5.internal.MockWebServerExtension
import okhttp3.OkHttpClientTestRule
import okhttp3.Request
import okhttp3.sse.EventSources.processResponse
import okhttp3.testing.PlatformRule
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.api.extension.RegisterExtension
@Tag("Slowish")
@ExtendWith(MockWebServerExtension::class)
class EventSourcesHttpTest {
@RegisterExtension
val platform = PlatformRule()
private lateinit var server: MockWebServer
@RegisterExtension
val clientTestRule = OkHttpClientTestRule()
private val listener = EventSourceRecorder()
private val client = clientTestRule.newClient()
@BeforeEach
fun before(server: MockWebServer) {
this.server = server
}
@AfterEach
fun after() {
listener.assertExhausted()
}
@Test
fun processResponse() {
server.enqueue(
MockResponse.Builder()
.body(
"""
|data: hey
|
|
""".trimMargin()
).setHeader("content-type", "text/event-stream")
.build()
)
val request = Request.Builder()
.url(server.url("/"))
.build()
val response = client.newCall(request).execute()
processResponse(response, listener)
listener.assertOpen()
listener.assertEvent(null, null, "hey")
listener.assertClose()
}
@Test
fun cancelShortCircuits() {
server.enqueue(
MockResponse.Builder()
.body(
"""
|data: hey
|
|
""".trimMargin()
).setHeader("content-type", "text/event-stream")
.build()
)
listener.enqueueCancel() // Will cancel in onOpen().
val request = Request.Builder()
.url(server.url("/"))
.build()
val response = client.newCall(request).execute()
processResponse(response, listener)
listener.assertOpen()
listener.assertFailure("canceled")
}
}

View File

@ -1,226 +0,0 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.sse.internal;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.Deque;
import javax.annotation.Nullable;
import okio.Buffer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
public final class ServerSentEventIteratorTest {
/** Either {@link Event} or {@link Long} items for events and retry changes, respectively. */
private final Deque<Object> callbacks = new ArrayDeque<>();
@AfterEach public void after() {
assertThat(callbacks).isEmpty();
}
@Test public void multiline() throws IOException {
consumeEvents(""
+ "data: YHOO\n"
+ "data: +2\n"
+ "data: 10\n"
+ "\n");
assertThat(callbacks.remove()).isEqualTo(new Event(null, null, "YHOO\n+2\n10"));
}
@Test public void multilineCr() throws IOException {
consumeEvents(""
+ "data: YHOO\r"
+ "data: +2\r"
+ "data: 10\r"
+ "\r");
assertThat(callbacks.remove()).isEqualTo(new Event(null, null, "YHOO\n+2\n10"));
}
@Test public void multilineCrLf() throws IOException {
consumeEvents(""
+ "data: YHOO\r\n"
+ "data: +2\r\n"
+ "data: 10\r\n"
+ "\r\n");
assertThat(callbacks.remove()).isEqualTo(new Event(null, null, "YHOO\n+2\n10"));
}
@Test public void eventType() throws IOException {
consumeEvents(""
+ "event: add\n"
+ "data: 73857293\n"
+ "\n"
+ "event: remove\n"
+ "data: 2153\n"
+ "\n"
+ "event: add\n"
+ "data: 113411\n"
+ "\n");
assertThat(callbacks.remove()).isEqualTo(new Event(null, "add", "73857293"));
assertThat(callbacks.remove()).isEqualTo(new Event(null, "remove", "2153"));
assertThat(callbacks.remove()).isEqualTo(new Event(null, "add", "113411"));
}
@Test public void commentsIgnored() throws IOException {
consumeEvents(""
+ ": test stream\n"
+ "\n"
+ "data: first event\n"
+ "id: 1\n"
+ "\n");
assertThat(callbacks.remove()).isEqualTo(new Event("1", null, "first event"));
}
@Test public void idCleared() throws IOException {
consumeEvents(""
+ "data: first event\n"
+ "id: 1\n"
+ "\n"
+ "data: second event\n"
+ "id\n"
+ "\n"
+ "data: third event\n"
+ "\n");
assertThat(callbacks.remove()).isEqualTo(new Event("1", null, "first event"));
assertThat(callbacks.remove()).isEqualTo(new Event(null, null, "second event"));
assertThat(callbacks.remove()).isEqualTo(new Event(null, null, "third event"));
}
@Test public void nakedFieldNames() throws IOException {
consumeEvents(""
+ "data\n"
+ "\n"
+ "data\n"
+ "data\n"
+ "\n"
+ "data:\n");
assertThat(callbacks.remove()).isEqualTo(new Event(null, null, ""));
assertThat(callbacks.remove()).isEqualTo(new Event(null, null, "\n"));
}
@Test public void colonSpaceOptional() throws IOException {
consumeEvents(""
+ "data:test\n"
+ "\n"
+ "data: test\n"
+ "\n");
assertThat(callbacks.remove()).isEqualTo(new Event(null, null, "test"));
assertThat(callbacks.remove()).isEqualTo(new Event(null, null, "test"));
}
@Test public void leadingWhitespace() throws IOException {
consumeEvents(""
+ "data: test\n"
+ "\n");
assertThat(callbacks.remove()).isEqualTo(new Event(null, null, " test"));
}
@Test public void idReusedAcrossEvents() throws IOException {
consumeEvents(""
+ "data: first event\n"
+ "id: 1\n"
+ "\n"
+ "data: second event\n"
+ "\n"
+ "id: 2\n"
+ "data: third event\n"
+ "\n");
assertThat(callbacks.remove()).isEqualTo(new Event("1", null, "first event"));
assertThat(callbacks.remove()).isEqualTo(new Event("1", null, "second event"));
assertThat(callbacks.remove()).isEqualTo(new Event("2", null, "third event"));
}
@Test public void idIgnoredFromEmptyEvent() throws IOException {
consumeEvents(""
+ "data: first event\n"
+ "id: 1\n"
+ "\n"
+ "id: 2\n"
+ "\n"
+ "data: second event\n"
+ "\n");
assertThat(callbacks.remove()).isEqualTo(new Event("1", null, "first event"));
assertThat(callbacks.remove()).isEqualTo(new Event("1", null, "second event"));
}
@Test public void retry() throws IOException {
consumeEvents(""
+ "retry: 22\n"
+ "\n"
+ "data: first event\n"
+ "id: 1\n"
+ "\n");
assertThat(callbacks.remove()).isEqualTo(22L);
assertThat(callbacks.remove()).isEqualTo(new Event("1", null, "first event"));
}
@Test public void retryInvalidFormatIgnored() throws IOException {
consumeEvents(""
+ "retry: 22\n"
+ "\n"
+ "retry: hey"
+ "\n");
assertThat(callbacks.remove()).isEqualTo(22L);
}
@Test public void namePrefixIgnored() throws IOException {
consumeEvents(""
+ "data: a\n"
+ "eventually\n"
+ "database\n"
+ "identity\n"
+ "retrying\n"
+ "\n");
assertThat(callbacks.remove()).isEqualTo(new Event(null, null, "a"));
}
@Test public void nakedNameClearsIdAndTypeAppendsData() throws IOException {
consumeEvents(""
+ "id: a\n"
+ "event: b\n"
+ "data: c\n"
+ "id\n"
+ "event\n"
+ "data\n"
+ "\n");
assertThat(callbacks.remove()).isEqualTo(new Event(null, null, "c\n"));
}
@Test public void nakedRetryIgnored() throws IOException {
consumeEvents(""
+ "retry\n"
+ "\n");
assertThat(callbacks).isEmpty();
}
private void consumeEvents(String source) throws IOException {
ServerSentEventReader.Callback callback = new ServerSentEventReader.Callback() {
@Override public void onEvent(@Nullable String id, @Nullable String type, String data) {
callbacks.add(new Event(id, type, data));
}
@Override public void onRetryChange(long timeMs) {
callbacks.add(timeMs);
}
};
Buffer buffer = new Buffer().writeUtf8(source);
ServerSentEventReader reader = new ServerSentEventReader(buffer, callback);
while (reader.processNextEvent());
assertThat(buffer.size()).overridingErrorMessage("Unconsumed buffer: " + buffer.readUtf8())
.isEqualTo(0);
}
}

View File

@ -0,0 +1,305 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.sse.internal
import java.util.ArrayDeque
import java.util.Deque
import okio.Buffer
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
class ServerSentEventIteratorTest {
/** Either [Event] or [Long] items for events and retry changes, respectively. */
private val callbacks: Deque<Any> = ArrayDeque()
@AfterEach
fun after() {
assertThat(callbacks).isEmpty()
}
@Test
fun multiline() {
consumeEvents(
"""
|data: YHOO
|data: +2
|data: 10
|
|
""".trimMargin()
)
assertThat(callbacks.remove()).isEqualTo(Event(null, null, "YHOO\n+2\n10"))
}
@Test
fun multilineCr() {
consumeEvents(
"""
|data: YHOO
|data: +2
|data: 10
|
|
""".trimMargin().replace("\n", "\r")
)
assertThat(callbacks.remove()).isEqualTo(Event(null, null, "YHOO\n+2\n10"))
}
@Test
fun multilineCrLf() {
consumeEvents(
"""
|data: YHOO
|data: +2
|data: 10
|
|
""".trimMargin().replace("\n", "\r\n")
)
assertThat(callbacks.remove()).isEqualTo(Event(null, null, "YHOO\n+2\n10"))
}
@Test
fun eventType() {
consumeEvents(
"""
|event: add
|data: 73857293
|
|event: remove
|data: 2153
|
|event: add
|data: 113411
|
|
""".trimMargin()
)
assertThat(callbacks.remove()).isEqualTo(Event(null, "add", "73857293"))
assertThat(callbacks.remove()).isEqualTo(Event(null, "remove", "2153"))
assertThat(callbacks.remove()).isEqualTo(Event(null, "add", "113411"))
}
@Test
fun commentsIgnored() {
consumeEvents(
"""
|: test stream
|
|data: first event
|id: 1
|
|
""".trimMargin()
)
assertThat(callbacks.remove()).isEqualTo(Event("1", null, "first event"))
}
@Test
fun idCleared() {
consumeEvents(
"""
|data: first event
|id: 1
|
|data: second event
|id
|
|data: third event
|
|
""".trimMargin()
)
assertThat(callbacks.remove()).isEqualTo(Event("1", null, "first event"))
assertThat(callbacks.remove()).isEqualTo(Event(null, null, "second event"))
assertThat(callbacks.remove()).isEqualTo(Event(null, null, "third event"))
}
@Test
fun nakedFieldNames() {
consumeEvents(
"""
|data
|
|data
|data
|
|data:
|
""".trimMargin()
)
assertThat(callbacks.remove()).isEqualTo(Event(null, null, ""))
assertThat(callbacks.remove()).isEqualTo(Event(null, null, "\n"))
}
@Test
fun colonSpaceOptional() {
consumeEvents(
"""
|data:test
|
|data: test
|
|
""".trimMargin()
)
assertThat(callbacks.remove()).isEqualTo(Event(null, null, "test"))
assertThat(callbacks.remove()).isEqualTo(Event(null, null, "test"))
}
@Test
fun leadingWhitespace() {
consumeEvents(
"""
|data: test
|
|
""".trimMargin()
)
assertThat(callbacks.remove()).isEqualTo(Event(null, null, " test"))
}
@Test
fun idReusedAcrossEvents() {
consumeEvents(
"""
|data: first event
|id: 1
|
|data: second event
|
|id: 2
|data: third event
|
|
""".trimMargin()
)
assertThat(callbacks.remove()).isEqualTo(Event("1", null, "first event"))
assertThat(callbacks.remove()).isEqualTo(Event("1", null, "second event"))
assertThat(callbacks.remove()).isEqualTo(Event("2", null, "third event"))
}
@Test
fun idIgnoredFromEmptyEvent() {
consumeEvents(
"""
|data: first event
|id: 1
|
|id: 2
|
|data: second event
|
|
""".trimMargin()
)
assertThat(callbacks.remove()).isEqualTo(Event("1", null, "first event"))
assertThat(callbacks.remove()).isEqualTo(Event("1", null, "second event"))
}
@Test
fun retry() {
consumeEvents(
"""
|retry: 22
|
|data: first event
|id: 1
|
|
""".trimMargin()
)
assertThat(callbacks.remove()).isEqualTo(22L)
assertThat(callbacks.remove()).isEqualTo(Event("1", null, "first event"))
}
@Test
fun retryInvalidFormatIgnored() {
consumeEvents(
"""
|retry: 22
|
|retry: hey
|
""".trimMargin()
)
assertThat(callbacks.remove()).isEqualTo(22L)
}
@Test
fun namePrefixIgnored() {
consumeEvents(
"""
|data: a
|eventually
|database
|identity
|retrying
|
|
""".trimMargin()
)
assertThat(callbacks.remove()).isEqualTo(Event(null, null, "a"))
}
@Test
fun nakedNameClearsIdAndTypeAppendsData() {
consumeEvents(
"""
|id: a
|event: b
|data: c
|id
|event
|data
|
|
""".trimMargin()
)
assertThat(callbacks.remove()).isEqualTo(Event(null, null, "c\n"))
}
@Test
fun nakedRetryIgnored() {
consumeEvents(
"""
|retry
|
""".trimMargin()
)
assertThat(callbacks).isEmpty()
}
private fun consumeEvents(source: String) {
val callback: ServerSentEventReader.Callback = object : ServerSentEventReader.Callback {
override fun onEvent(id: String?, type: String?, data: String) {
callbacks.add(Event(id, type, data))
}
override fun onRetryChange(timeMs: Long) {
callbacks.add(timeMs)
}
}
val buffer = Buffer().writeUtf8(source)
val reader = ServerSentEventReader(buffer, callback)
while (reader.processNextEvent()) {
}
assertThat(buffer.size)
.overridingErrorMessage("Unconsumed buffer: ${buffer.readUtf8()}")
.isEqualTo(0)
}
}

View File

@ -1,506 +0,0 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.tls;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.concurrent.TimeUnit;
import okhttp3.testing.PlatformRule;
import okio.ByteString;
import org.bouncycastle.asn1.x509.GeneralName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.data.Offset.offset;
import static org.junit.jupiter.api.Assertions.fail;
public final class HeldCertificateTest {
@RegisterExtension public PlatformRule platform = new PlatformRule();
@Test public void defaultCertificate() throws CertificateParsingException {
long now = System.currentTimeMillis();
HeldCertificate heldCertificate = new HeldCertificate.Builder().build();
X509Certificate certificate = heldCertificate.certificate();
assertThat(certificate.getSubjectX500Principal().getName()).overridingErrorMessage(
"self-signed").isEqualTo(certificate.getIssuerX500Principal().getName());
assertThat(certificate.getIssuerX500Principal().getName()).matches("CN=[0-9a-f-]{36}");
assertThat(certificate.getSerialNumber()).isEqualTo(BigInteger.ONE);
assertThat(certificate.getSubjectAlternativeNames()).isNull();
double deltaMillis = 1000.0;
long durationMillis = TimeUnit.MINUTES.toMillis(60 * 24);
assertThat((double) certificate.getNotBefore().getTime()).isCloseTo(
(double) now, offset(deltaMillis));
assertThat((double) certificate.getNotAfter().getTime()).isCloseTo(
(double) now + durationMillis, offset(deltaMillis));
}
@Test public void customInterval() {
// 5 seconds starting on 1970-01-01.
HeldCertificate heldCertificate = new HeldCertificate.Builder()
.validityInterval(5_000L, 10_000L)
.build();
X509Certificate certificate = heldCertificate.certificate();
assertThat(certificate.getNotBefore().getTime()).isEqualTo(5_000L);
assertThat(certificate.getNotAfter().getTime()).isEqualTo(10_000L);
}
@Test public void customDuration() {
long now = System.currentTimeMillis();
HeldCertificate heldCertificate = new HeldCertificate.Builder()
.duration(5, TimeUnit.SECONDS)
.build();
X509Certificate certificate = heldCertificate.certificate();
double deltaMillis = 1000.0;
long durationMillis = 5_000L;
assertThat((double) certificate.getNotBefore().getTime()).isCloseTo(
(double) now, offset(deltaMillis));
assertThat((double) certificate.getNotAfter().getTime()).isCloseTo(
(double) now + durationMillis, offset(deltaMillis));
}
@Test public void subjectAlternativeNames() throws CertificateParsingException {
HeldCertificate heldCertificate = new HeldCertificate.Builder()
.addSubjectAlternativeName("1.1.1.1")
.addSubjectAlternativeName("cash.app")
.build();
X509Certificate certificate = heldCertificate.certificate();
assertThat(certificate.getSubjectAlternativeNames()).containsExactly(
asList(GeneralName.iPAddress, "1.1.1.1"),
asList(GeneralName.dNSName, "cash.app"));
}
@Test public void commonName() {
HeldCertificate heldCertificate = new HeldCertificate.Builder()
.commonName("cash.app")
.build();
X509Certificate certificate = heldCertificate.certificate();
assertThat(certificate.getSubjectX500Principal().getName()).isEqualTo("CN=cash.app");
}
@Test public void organizationalUnit() {
HeldCertificate heldCertificate = new HeldCertificate.Builder()
.commonName("cash.app")
.organizationalUnit("cash")
.build();
X509Certificate certificate = heldCertificate.certificate();
assertThat(certificate.getSubjectX500Principal().getName()).isEqualTo(
"CN=cash.app,OU=cash");
}
/** Confirm golden values of encoded PEMs. */
@Test public void pems() throws Exception {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
ByteString publicKeyBytes = ByteString.decodeBase64("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCApF"
+ "HhtrLan28q+oMolZuaTfWBA0V5aMIvq32BsloQu6LlvX1wJ4YEoUCjDlPOtpht7XLbUmBnbIzN89XK4UJVM6Sqp3"
+ "K88Km8z7gMrdrfTom/274wL25fICR+yDEQ5fUVYBmJAKXZF1aoI0mIoEx0xFsQhIJ637v2MxJDupd61wIDAQAB");
PublicKey publicKey = keyFactory.generatePublic(
new X509EncodedKeySpec(publicKeyBytes.toByteArray()));
ByteString privateKeyBytes = ByteString.decodeBase64("MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbA"
+ "gEAAoGBAICkUeG2stqfbyr6gyiVm5pN9YEDRXlowi+rfYGyWhC7ouW9fXAnhgShQKMOU862mG3tcttSYGdsjM3z1"
+ "crhQlUzpKqncrzwqbzPuAyt2t9Oib/bvjAvbl8gJH7IMRDl9RVgGYkApdkXVqgjSYigTHTEWxCEgnrfu/YzEkO6l"
+ "3rXAgMBAAECgYB99mhnB6piADOuddXv626NzUBTr4xbsYRTgSxHzwf50oFTTBSDuW+1IOBVyTWu94SSPyt0LllPb"
+ "C8Di3sQSTnVGpSqAvEXknBMzIc0UO74Rn9p3gZjEenPt1l77fIBa2nK06/rdsJCoE/1P1JSfM9w7LU1RsTmseYML"
+ "eJl5F79gQJBAO/BbAKqg1yzK7VijygvBoUrr+rt2lbmKgcUQ/rxu8IIQk0M/xgJqSkXDXuOnboGM7sQSKfJAZUtT"
+ "7xozvLzV7ECQQCJW59w7NIM0qZ/gIX2gcNZr1B/V3zcGlolTDciRm+fnKGNt2EEDKnVL3swzbEfTCa48IT0QKgZJ"
+ "qpXZERa26UHAkBLXmiP5f5pk8F3wcXzAeVw06z3k1IB41Tu6MX+CyPU+TeudRlz+wV8b0zDvK+EnRKCCbptVFj1B"
+ "kt8lQ4JfcnhAkAk2Y3Gz+HySrkcT7Cg12M/NkdUQnZe3jr88pt/+IGNwomc6Wt/mJ4fcWONTkGMcfOZff1NQeNXD"
+ "AZ6941XCsIVAkASOg02PlVHLidU7mIE65swMM5/RNhS4aFjez/MwxFNOHaxc9VgCwYPXCLOtdf7AVovdyG0XWgbU"
+ "XH+NyxKwboE");
PrivateKey privateKey = keyFactory.generatePrivate(
new PKCS8EncodedKeySpec(privateKeyBytes.toByteArray()));
HeldCertificate heldCertificate = new HeldCertificate.Builder()
.keyPair(publicKey, privateKey)
.commonName("cash.app")
.validityInterval(0L, 1_000L)
.rsa2048()
.build();
assertThat(""
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIIBmjCCAQOgAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDDAhjYXNo\n"
+ "LmFwcDAeFw03MDAxMDEwMDAwMDBaFw03MDAxMDEwMDAwMDFaMBMxETAPBgNVBAMM\n"
+ "CGNhc2guYXBwMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCApFHhtrLan28q\n"
+ "+oMolZuaTfWBA0V5aMIvq32BsloQu6LlvX1wJ4YEoUCjDlPOtpht7XLbUmBnbIzN\n"
+ "89XK4UJVM6Sqp3K88Km8z7gMrdrfTom/274wL25fICR+yDEQ5fUVYBmJAKXZF1ao\n"
+ "I0mIoEx0xFsQhIJ637v2MxJDupd61wIDAQABMA0GCSqGSIb3DQEBCwUAA4GBADHT\n"
+ "vcjwl9Z4I5Cb2R1y7aaa860HkY2k3ThaDK5OJt6GYqJTA9P3LtX7VwQtL1TWqXGc\n"
+ "+OEfl3zhm0PUqcbckMzhJtqIa7NkDSjNm71BKd843pIhGcEri69DcL/cR8T+eMex\n"
+ "hadh7aGM9OjeL8gznLeq27Ly6Dj7Vkp5OmOrSKfn\n"
+ "-----END CERTIFICATE-----\n").isEqualTo(heldCertificate.certificatePem());
assertThat((""
+ "-----BEGIN RSA PRIVATE KEY-----\n"
+ "MIICWwIBAAKBgQCApFHhtrLan28q+oMolZuaTfWBA0V5aMIvq32BsloQu6LlvX1w\n"
+ "J4YEoUCjDlPOtpht7XLbUmBnbIzN89XK4UJVM6Sqp3K88Km8z7gMrdrfTom/274w\n"
+ "L25fICR+yDEQ5fUVYBmJAKXZF1aoI0mIoEx0xFsQhIJ637v2MxJDupd61wIDAQAB\n"
+ "AoGAffZoZweqYgAzrnXV7+tujc1AU6+MW7GEU4EsR88H+dKBU0wUg7lvtSDgVck1\n"
+ "rveEkj8rdC5ZT2wvA4t7EEk51RqUqgLxF5JwTMyHNFDu+EZ/ad4GYxHpz7dZe+3y\n"
+ "AWtpytOv63bCQqBP9T9SUnzPcOy1NUbE5rHmDC3iZeRe/YECQQDvwWwCqoNcsyu1\n"
+ "Yo8oLwaFK6/q7dpW5ioHFEP68bvCCEJNDP8YCakpFw17jp26BjO7EEinyQGVLU+8\n"
+ "aM7y81exAkEAiVufcOzSDNKmf4CF9oHDWa9Qf1d83BpaJUw3IkZvn5yhjbdhBAyp\n"
+ "1S97MM2xH0wmuPCE9ECoGSaqV2REWtulBwJAS15oj+X+aZPBd8HF8wHlcNOs95NS\n"
+ "AeNU7ujF/gsj1Pk3rnUZc/sFfG9Mw7yvhJ0Sggm6bVRY9QZLfJUOCX3J4QJAJNmN\n"
+ "xs/h8kq5HE+woNdjPzZHVEJ2Xt46/PKbf/iBjcKJnOlrf5ieH3FjjU5BjHHzmX39\n"
+ "TUHjVwwGeveNVwrCFQJAEjoNNj5VRy4nVO5iBOubMDDOf0TYUuGhY3s/zMMRTTh2\n"
+ "sXPVYAsGD1wizrXX+wFaL3chtF1oG1Fx/jcsSsG6BA==\n"
+ "-----END RSA PRIVATE KEY-----\n")).isEqualTo(heldCertificate.privateKeyPkcs1Pem());
assertThat((""
+ "-----BEGIN PRIVATE KEY-----\n"
+ "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAICkUeG2stqfbyr6\n"
+ "gyiVm5pN9YEDRXlowi+rfYGyWhC7ouW9fXAnhgShQKMOU862mG3tcttSYGdsjM3z\n"
+ "1crhQlUzpKqncrzwqbzPuAyt2t9Oib/bvjAvbl8gJH7IMRDl9RVgGYkApdkXVqgj\n"
+ "SYigTHTEWxCEgnrfu/YzEkO6l3rXAgMBAAECgYB99mhnB6piADOuddXv626NzUBT\n"
+ "r4xbsYRTgSxHzwf50oFTTBSDuW+1IOBVyTWu94SSPyt0LllPbC8Di3sQSTnVGpSq\n"
+ "AvEXknBMzIc0UO74Rn9p3gZjEenPt1l77fIBa2nK06/rdsJCoE/1P1JSfM9w7LU1\n"
+ "RsTmseYMLeJl5F79gQJBAO/BbAKqg1yzK7VijygvBoUrr+rt2lbmKgcUQ/rxu8II\n"
+ "Qk0M/xgJqSkXDXuOnboGM7sQSKfJAZUtT7xozvLzV7ECQQCJW59w7NIM0qZ/gIX2\n"
+ "gcNZr1B/V3zcGlolTDciRm+fnKGNt2EEDKnVL3swzbEfTCa48IT0QKgZJqpXZERa\n"
+ "26UHAkBLXmiP5f5pk8F3wcXzAeVw06z3k1IB41Tu6MX+CyPU+TeudRlz+wV8b0zD\n"
+ "vK+EnRKCCbptVFj1Bkt8lQ4JfcnhAkAk2Y3Gz+HySrkcT7Cg12M/NkdUQnZe3jr8\n"
+ "8pt/+IGNwomc6Wt/mJ4fcWONTkGMcfOZff1NQeNXDAZ6941XCsIVAkASOg02PlVH\n"
+ "LidU7mIE65swMM5/RNhS4aFjez/MwxFNOHaxc9VgCwYPXCLOtdf7AVovdyG0XWgb\n"
+ "UXH+NyxKwboE\n"
+ "-----END PRIVATE KEY-----\n")).isEqualTo(heldCertificate.privateKeyPkcs8Pem());
}
@Test public void ecdsaSignedByRsa() {
HeldCertificate root = new HeldCertificate.Builder()
.certificateAuthority(0)
.rsa2048()
.build();
HeldCertificate leaf = new HeldCertificate.Builder()
.certificateAuthority(0)
.ecdsa256()
.signedBy(root)
.build();
assertThat(root.certificate().getSigAlgName()).isEqualToIgnoringCase("SHA256WITHRSA");
assertThat(leaf.certificate().getSigAlgName()).isEqualToIgnoringCase("SHA256WITHRSA");
}
@Test public void rsaSignedByEcdsa() {
HeldCertificate root = new HeldCertificate.Builder()
.certificateAuthority(0)
.ecdsa256()
.build();
HeldCertificate leaf = new HeldCertificate.Builder()
.certificateAuthority(0)
.rsa2048()
.signedBy(root)
.build();
assertThat(root.certificate().getSigAlgName()).isEqualToIgnoringCase("SHA256WITHECDSA");
assertThat(leaf.certificate().getSigAlgName()).isEqualToIgnoringCase("SHA256WITHECDSA");
}
@Test public void decodeEcdsa256() throws Exception {
// The certificate + private key below was generated programmatically:
//
// HeldCertificate heldCertificate = new HeldCertificate.Builder()
// .validityInterval(5_000L, 10_000L)
// .addSubjectAlternativeName("1.1.1.1")
// .addSubjectAlternativeName("cash.app")
// .serialNumber(42L)
// .commonName("cash.app")
// .organizationalUnit("engineering")
// .ecdsa256()
// .build();
String certificatePem = ""
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIIBYTCCAQegAwIBAgIBKjAKBggqhkjOPQQDAjApMRQwEgYDVQQLEwtlbmdpbmVl\n"
+ "cmluZzERMA8GA1UEAxMIY2FzaC5hcHAwHhcNNzAwMTAxMDAwMDA1WhcNNzAwMTAx\n"
+ "MDAwMDEwWjApMRQwEgYDVQQLEwtlbmdpbmVlcmluZzERMA8GA1UEAxMIY2FzaC5h\n"
+ "cHAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASda8ChkQXxGELnrV/oBnIAx3dD\n"
+ "ocUOJfdz4pOJTP6dVQB9U3UBiW5uSX/MoOD0LL5zG3bVyL3Y6pDwKuYvfLNhoyAw\n"
+ "HjAcBgNVHREBAf8EEjAQhwQBAQEBgghjYXNoLmFwcDAKBggqhkjOPQQDAgNIADBF\n"
+ "AiAyHHg1N6YDDQiY920+cnI5XSZwEGhAtb9PYWO8bLmkcQIhAI2CfEZf3V/obmdT\n"
+ "yyaoEufLKVXhrTQhRfodTeigi4RX\n"
+ "-----END CERTIFICATE-----\n";
String pkcs8Pem = ""
+ "-----BEGIN PRIVATE KEY-----\n"
+ "MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCA7ODT0xhGSNn4ESj6J\n"
+ "lu/GJQZoU9lDrCPeUcQ28tzOWw==\n"
+ "-----END PRIVATE KEY-----\n";
String bcPkcs8Pem = ""
+ "-----BEGIN PRIVATE KEY-----\n"
+ "ME0CAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEMzAxAgEBBCA7ODT0xhGSNn4ESj6J\n"
+ "lu/GJQZoU9lDrCPeUcQ28tzOW6AKBggqhkjOPQMBBw==\n"
+ "-----END PRIVATE KEY-----\n";
HeldCertificate heldCertificate = HeldCertificate.decode(certificatePem + pkcs8Pem);
assertThat(heldCertificate.certificatePem()).isEqualTo(certificatePem);
// Slightly different encoding
if (platform.isBouncyCastle()) {
assertThat(heldCertificate.privateKeyPkcs8Pem()).isEqualTo(bcPkcs8Pem);
} else {
assertThat(heldCertificate.privateKeyPkcs8Pem()).isEqualTo(pkcs8Pem);
}
X509Certificate certificate = heldCertificate.certificate();
assertThat(certificate.getNotBefore().getTime()).isEqualTo(5_000L);
assertThat(certificate.getNotAfter().getTime()).isEqualTo(10_000L);
assertThat(certificate.getSubjectAlternativeNames()).containsExactly(
asList(GeneralName.iPAddress, "1.1.1.1"),
asList(GeneralName.dNSName, "cash.app"));
assertThat(certificate.getSubjectX500Principal().getName())
.isEqualTo("CN=cash.app,OU=engineering");
}
@Test public void decodeRsa512() {
// The certificate + private key below was generated with OpenSSL. Never generate certificates
// with MD5 or 512-bit RSA; that's insecure!
//
// openssl req \
// -x509 \
// -md5 \
// -nodes \
// -days 1 \
// -newkey rsa:512 \
// -keyout privateKey.key \
// -out certificate.crt
String certificatePem = ""
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIIBFzCBwgIJAIVAqagcVN7/MA0GCSqGSIb3DQEBBAUAMBMxETAPBgNVBAMMCGNh\n"
+ "c2guYXBwMB4XDTE5MDkwNzAyMjg0NFoXDTE5MDkwODAyMjg0NFowEzERMA8GA1UE\n"
+ "AwwIY2FzaC5hcHAwXDANBgkqhkiG9w0BAQEFAANLADBIAkEA8qAeoubm4mBTD9/J\n"
+ "ujLQkfk/fuJt/T5pVQ1vUEqxfcMw0zYgszQ5C2MiIl7M6JkTRKU01q9hVFCR83wX\n"
+ "zIdrLQIDAQABMA0GCSqGSIb3DQEBBAUAA0EAO1UpwhrkW3Ho1nZK/taoUQOoqz/n\n"
+ "HFVMtyEkm5gBDgz8nJXwb3zbegclQyH+kVou02S8zC5WWzEtd0R8S0LsTA==\n"
+ "-----END CERTIFICATE-----\n";
String pkcs8Pem = ""
+ "-----BEGIN PRIVATE KEY-----\n"
+ "MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEA8qAeoubm4mBTD9/J\n"
+ "ujLQkfk/fuJt/T5pVQ1vUEqxfcMw0zYgszQ5C2MiIl7M6JkTRKU01q9hVFCR83wX\n"
+ "zIdrLQIDAQABAkEA7dEA9o/5k77y68ZhRv9z7QEwucBcKzQ3rsSCbWMpYqg924F9\n"
+ "L8Z76kzSedSO2PN8mg6y/OLL+qBuTeUK/yiowQIhAP0cknFMbqeNX6uvj/S+V7in\n"
+ "bIhQkhcSdJjRw8fxMnJpAiEA9WTp9wzJpn+9etZo0jJ8wkM0+LTMNELo47Ctz7l1\n"
+ "kiUCIQCi34vslD5wWyzBEcwUtZdFH5dbcF1Rs3KMFA9jzfWkYQIgHtiWiFV1K5a3\n"
+ "DK/S8UkjYY/tIq4nVRJsD+LvlkLrwnkCIECcz4yF4HQgv+Tbzj/gGSBl1VIliTcB\n"
+ "Rc5RUQ0mZJQF\n"
+ "-----END PRIVATE KEY-----\n";
HeldCertificate heldCertificate = HeldCertificate.decode(pkcs8Pem + certificatePem);
assertThat(heldCertificate.certificatePem()).isEqualTo(certificatePem);
assertThat(heldCertificate.privateKeyPkcs8Pem()).isEqualTo(pkcs8Pem);
X509Certificate certificate = heldCertificate.certificate();
assertThat(certificate.getSubjectX500Principal().getName())
.isEqualTo("CN=cash.app");
}
@Test public void decodeRsa2048() throws Exception {
// The certificate + private key below was generated programmatically:
//
// HeldCertificate heldCertificate = new HeldCertificate.Builder()
// .validityInterval(5_000L, 10_000L)
// .addSubjectAlternativeName("1.1.1.1")
// .addSubjectAlternativeName("cash.app")
// .serialNumber(42L)
// .commonName("cash.app")
// .organizationalUnit("engineering")
// .rsa2048()
// .build();
String certificatePem = ""
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIIC7TCCAdWgAwIBAgIBKjANBgkqhkiG9w0BAQsFADApMRQwEgYDVQQLEwtlbmdp\n"
+ "bmVlcmluZzERMA8GA1UEAxMIY2FzaC5hcHAwHhcNNzAwMTAxMDAwMDA1WhcNNzAw\n"
+ "MTAxMDAwMDEwWjApMRQwEgYDVQQLEwtlbmdpbmVlcmluZzERMA8GA1UEAxMIY2Fz\n"
+ "aC5hcHAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCaU+vrUPL0APGI\n"
+ "SXIuRX4xRrigXmGKx+GRPnWDWvGJwOm23Vpq/eZxQx6PbSUB1+QZzAwge20RpNAp\n"
+ "2lt5/qFtgUpEon2j06rd/0+ODqqVJX+6d3SpmF1fPfKUB6AOZbxEkaJpBSTavoTg\n"
+ "G2M/NMdjZjrcB3quNQcLg54mmI3HJm1zOd/8i2fZjvoiyVY30Inn2SmQsAotXw1u\n"
+ "aE/319bnR2sQlnkp6MJU0eLEtKyRif/IODvY+mtRYYdkFtoeT6qQPMIh+gF/H3to\n"
+ "5tjs3g59QC8k2TJDop4EFYUOwdrtnb8wUiBnLyURD1szASE2IO2Ftk1zaNOPKtrv\n"
+ "VeJuB/mpAgMBAAGjIDAeMBwGA1UdEQEB/wQSMBCHBAEBAQGCCGNhc2guYXBwMA0G\n"
+ "CSqGSIb3DQEBCwUAA4IBAQAPm7vfk+rxSucxxbFiimmFKBw+ymieLY/kznNh0lHJ\n"
+ "q15fsMYK7TTTt2FFqyfVXhhRZegLrkrGb3+4Dz1uNtcRrjT4qo+T/JOuZGxtBLbm\n"
+ "4/hkFSYavtd2FW+/CK7EnQKUyabgLOblb21IHOlcPwpSe6KkJjpwq0TV/ozzfk/q\n"
+ "kGRA7/Ubn5TMRYyHWnod2SS14+BkItcWN03Z7kvyMYrpNZpu6vQRYsqJJFMcmpGZ\n"
+ "sZQW31gO2arPmfNotkQdFdNL12c9YZKkJGhyK6NcpffD2l6O9NS5SRD5RnkvBxQw\n"
+ "fX5DamL8je/YKSLQ4wgUA/5iVKlCiJGQi6fYIJ0kxayO\n"
+ "-----END CERTIFICATE-----\n";
String pkcs8Pem = ""
+ "-----BEGIN PRIVATE KEY-----\n"
+ "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCaU+vrUPL0APGI\n"
+ "SXIuRX4xRrigXmGKx+GRPnWDWvGJwOm23Vpq/eZxQx6PbSUB1+QZzAwge20RpNAp\n"
+ "2lt5/qFtgUpEon2j06rd/0+ODqqVJX+6d3SpmF1fPfKUB6AOZbxEkaJpBSTavoTg\n"
+ "G2M/NMdjZjrcB3quNQcLg54mmI3HJm1zOd/8i2fZjvoiyVY30Inn2SmQsAotXw1u\n"
+ "aE/319bnR2sQlnkp6MJU0eLEtKyRif/IODvY+mtRYYdkFtoeT6qQPMIh+gF/H3to\n"
+ "5tjs3g59QC8k2TJDop4EFYUOwdrtnb8wUiBnLyURD1szASE2IO2Ftk1zaNOPKtrv\n"
+ "VeJuB/mpAgMBAAECggEAOlOXaYNZn1Cv+INRrR1EmVkSNEIXeX0bymohvbhka1zG\n"
+ "t/8myiMVsh7c8PYeM3kl034j4y7ixPVWW0sUoaHT3vArYo9LDtzTyj1REu6GGAJp\n"
+ "KM82/1X/jBx8jufm3SokIoIsMKbqC+ZPj+ep9dx7sxyTCE+nVSnjdL2Uyx+DDg3o\n"
+ "na237HTScWIi+tMv5QGEwqLHS2q+NZYfjgnSxNY8BRw4XZCcIZRko9niuB5gUjj/\n"
+ "y01HwvOCWuOMaSKZak1OdOaz3427/TkhYIqf6ft0ELF+ASRk3BLQA06pRt88H3u2\n"
+ "3vsHJsWr2rkCN0h9uDp2o50ZQ5fvlxqG0QIZmvkIkQKBgQDOHeZKvXO5IxQ+S8ed\n"
+ "09bC5SKiclCdW+Ry7N2x1MBfrxc4TTTTNaUN9Qdc6RXANG9bX2CJv0Dkh/0yH3z9\n"
+ "Bdq6YcoP6DFCX46jwhCKvxMX9h9PFLvY7l2VSe7NfboGzvYLCy8ErsGuio8u9MHZ\n"
+ "osX2ch6Gdhn1xUwLCw+T7rNwjQKBgQC/rWb0sWfgbKhEqV+u5oov+fFjooWmTQlQ\n"
+ "jcj+lMWUOkitnPmX9TsH5JDa8I89Y0gJGu7Lfg8XSH+4FCCfX3mSLYwVH5vAIvmr\n"
+ "TjMqRwSahQuTr/g+lx7alpcUHYv3z6b3WYIXFPPr3t7grWNJ14wMv9DnItWOg84H\n"
+ "LlxAvXXsjQKBgQCRPPhdignVVyaYjwVl7TPTuWoiVbMAbxQW91lwSZ4UzmfqQF0M\n"
+ "xyw7HYHGsmelPE2LcTWxWpb7cee0PgPwtwNdejLL6q1rO7JjKghF/EYUCFYff1iu\n"
+ "j6hZ3fLr0cAXtBYjygmjnxDTUMd8KvO9y7j644cm8GlyiUgAMBcWAolmsQKBgQCT\n"
+ "AJQTWfPGxM6QSi3d32VfwhsFROGnVzGrm/HofYTCV6jhraAmkKcDOKJ3p0LT286l\n"
+ "XQiC/FzqiGmbbaRPVlPQbiofESzMQIamgMTwyaKYNy1XyP9kUVYSYqfff4GXPqRY\n"
+ "00bYGPOxlC3utkuNmEgKhxnaCncqY5+hFkceR6+nCQKBgQC1Gonjhw0lYe43aHpp\n"
+ "nDJKv3FnyN3wxjsR2c9sWpDzHA6CMVhSeLoXCB9ishmrSE/CygNlTU1TEy63xN22\n"
+ "+dMHl5I/urMesjKKWiKZHdbWVIjJDv25r3jrN9VLr4q6AD9r1Su5G0o2j0N5ujVg\n"
+ "SzpFHp+ZzhL/SANa8EqlcF6ItQ==\n"
+ "-----END PRIVATE KEY-----\n";
HeldCertificate heldCertificate = HeldCertificate.decode(pkcs8Pem + certificatePem);
assertThat(heldCertificate.certificatePem()).isEqualTo(certificatePem);
assertThat(heldCertificate.privateKeyPkcs8Pem()).isEqualTo(pkcs8Pem);
X509Certificate certificate = heldCertificate.certificate();
assertThat(certificate.getNotBefore().getTime()).isEqualTo(5_000L);
assertThat(certificate.getNotAfter().getTime()).isEqualTo(10_000L);
assertThat(certificate.getSubjectAlternativeNames()).containsExactly(
asList(GeneralName.iPAddress, "1.1.1.1"),
asList(GeneralName.dNSName, "cash.app"));
assertThat(certificate.getSubjectX500Principal().getName())
.isEqualTo("CN=cash.app,OU=engineering");
}
@Test public void decodeWrongNumber() {
String certificatePem = ""
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIIBYTCCAQegAwIBAgIBKjAKBggqhkjOPQQDAjApMRQwEgYDVQQLEwtlbmdpbmVl\n"
+ "cmluZzERMA8GA1UEAxMIY2FzaC5hcHAwHhcNNzAwMTAxMDAwMDA1WhcNNzAwMTAx\n"
+ "MDAwMDEwWjApMRQwEgYDVQQLEwtlbmdpbmVlcmluZzERMA8GA1UEAxMIY2FzaC5h\n"
+ "cHAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASda8ChkQXxGELnrV/oBnIAx3dD\n"
+ "ocUOJfdz4pOJTP6dVQB9U3UBiW5uSX/MoOD0LL5zG3bVyL3Y6pDwKuYvfLNhoyAw\n"
+ "HjAcBgNVHREBAf8EEjAQhwQBAQEBgghjYXNoLmFwcDAKBggqhkjOPQQDAgNIADBF\n"
+ "AiAyHHg1N6YDDQiY920+cnI5XSZwEGhAtb9PYWO8bLmkcQIhAI2CfEZf3V/obmdT\n"
+ "yyaoEufLKVXhrTQhRfodTeigi4RX\n"
+ "-----END CERTIFICATE-----\n";
String pkcs8Pem = ""
+ "-----BEGIN PRIVATE KEY-----\n"
+ "MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCA7ODT0xhGSNn4ESj6J\n"
+ "lu/GJQZoU9lDrCPeUcQ28tzOWw==\n"
+ "-----END PRIVATE KEY-----\n";
try {
HeldCertificate.decode(certificatePem);
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("string does not include a private key");
}
try {
HeldCertificate.decode(pkcs8Pem);
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("string does not include a certificate");
}
try {
HeldCertificate.decode(certificatePem + pkcs8Pem + certificatePem);
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("string includes multiple certificates");
}
try {
HeldCertificate.decode(pkcs8Pem + certificatePem + pkcs8Pem);
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("string includes multiple private keys");
}
}
@Test public void decodeWrongType() {
try {
HeldCertificate.decode(""
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIIBmjCCAQOgAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhjYXNo\n"
+ "-----END CERTIFICATE-----\n"
+ "-----BEGIN RSA PRIVATE KEY-----\n"
+ "sXPVYAsGD1wizrXX+wFaL3chtF1oG1Fx/jcsSsG6BA==\n"
+ "-----END RSA PRIVATE KEY-----\n");
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("unexpected type: RSA PRIVATE KEY");
}
}
@Test public void decodeMalformed() {
try {
HeldCertificate.decode(""
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIIBYTCCAQegAwIBAgIBKjAKBggqhkjOPQQDAjApMRQwEgYDVQQLEwtlbmdpbmVl\n"
+ "-----END CERTIFICATE-----\n"
+ "-----BEGIN PRIVATE KEY-----\n"
+ "MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCA7ODT0xhGSNn4ESj6J\n"
+ "lu/GJQZoU9lDrCPeUcQ28tzOWw==\n"
+ "-----END PRIVATE KEY-----\n");
fail();
} catch (IllegalArgumentException expected) {
if (!platform.isConscrypt()) {
assertThat(expected).hasMessage("failed to decode certificate");
}
}
try {
HeldCertificate.decode(""
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIIBYTCCAQegAwIBAgIBKjAKBggqhkjOPQQDAjApMRQwEgYDVQQLEwtlbmdpbmVl\n"
+ "cmluZzERMA8GA1UEAxMIY2FzaC5hcHAwHhcNNzAwMTAxMDAwMDA1WhcNNzAwMTAx\n"
+ "MDAwMDEwWjApMRQwEgYDVQQLEwtlbmdpbmVlcmluZzERMA8GA1UEAxMIY2FzaC5h\n"
+ "cHAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASda8ChkQXxGELnrV/oBnIAx3dD\n"
+ "ocUOJfdz4pOJTP6dVQB9U3UBiW5uSX/MoOD0LL5zG3bVyL3Y6pDwKuYvfLNhoyAw\n"
+ "HjAcBgNVHREBAf8EEjAQhwQBAQEBgghjYXNoLmFwcDAKBggqhkjOPQQDAgNIADBF\n"
+ "AiAyHHg1N6YDDQiY920+cnI5XSZwEGhAtb9PYWO8bLmkcQIhAI2CfEZf3V/obmdT\n"
+ "yyaoEufLKVXhrTQhRfodTeigi4RX\n"
+ "-----END CERTIFICATE-----\n"
+ "-----BEGIN PRIVATE KEY-----\n"
+ "MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCA7ODT0xhGSNn4ESj6J\n"
+ "-----END PRIVATE KEY-----\n");
fail();
} catch (IllegalArgumentException expected) {
if (!platform.isConscrypt()) {
assertThat(expected).hasMessage("failed to decode private key");
}
}
}
}

View File

@ -0,0 +1,522 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.tls
import java.math.BigInteger
import java.security.KeyFactory
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
import java.util.Arrays
import java.util.concurrent.TimeUnit
import okhttp3.testing.PlatformRule
import okhttp3.tls.HeldCertificate.Companion.decode
import okio.ByteString.Companion.decodeBase64
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.data.Offset
import org.bouncycastle.asn1.x509.GeneralName
import org.junit.jupiter.api.Assertions.fail
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
class HeldCertificateTest {
@RegisterExtension
var platform = PlatformRule()
@Test
fun defaultCertificate() {
val now = System.currentTimeMillis()
val heldCertificate = HeldCertificate.Builder().build()
val certificate = heldCertificate.certificate
assertThat(certificate.getSubjectX500Principal().name).overridingErrorMessage(
"self-signed"
).isEqualTo(certificate.getIssuerX500Principal().name)
assertThat(certificate.getIssuerX500Principal().name).matches("CN=[0-9a-f-]{36}")
assertThat(certificate.serialNumber).isEqualTo(BigInteger.ONE)
assertThat(certificate.subjectAlternativeNames).isNull()
val deltaMillis = 1000.0
val durationMillis = TimeUnit.MINUTES.toMillis((60 * 24).toLong())
assertThat(certificate.notBefore.time.toDouble())
.isCloseTo(now.toDouble(), Offset.offset(deltaMillis))
assertThat(certificate.notAfter.time.toDouble()).isCloseTo(
now.toDouble() + durationMillis, Offset.offset(deltaMillis)
)
}
@Test
fun customInterval() {
// 5 seconds starting on 1970-01-01.
val heldCertificate = HeldCertificate.Builder()
.validityInterval(5000L, 10000L)
.build()
val certificate = heldCertificate.certificate
assertThat(certificate.notBefore.time).isEqualTo(5000L)
assertThat(certificate.notAfter.time).isEqualTo(10000L)
}
@Test
fun customDuration() {
val now = System.currentTimeMillis()
val heldCertificate = HeldCertificate.Builder()
.duration(5, TimeUnit.SECONDS)
.build()
val certificate = heldCertificate.certificate
val deltaMillis = 1000.0
val durationMillis = 5000L
assertThat(certificate.notBefore.time.toDouble())
.isCloseTo(now.toDouble(), Offset.offset(deltaMillis))
assertThat(certificate.notAfter.time.toDouble()).isCloseTo(
now.toDouble() + durationMillis, Offset.offset(deltaMillis)
)
}
@Test
fun subjectAlternativeNames() {
val heldCertificate = HeldCertificate.Builder()
.addSubjectAlternativeName("1.1.1.1")
.addSubjectAlternativeName("cash.app")
.build()
val certificate = heldCertificate.certificate
assertThat(certificate.subjectAlternativeNames).containsExactly(
listOf(GeneralName.iPAddress, "1.1.1.1"),
listOf(GeneralName.dNSName, "cash.app")
)
}
@Test
fun commonName() {
val heldCertificate = HeldCertificate.Builder()
.commonName("cash.app")
.build()
val certificate = heldCertificate.certificate
assertThat(certificate.getSubjectX500Principal().name).isEqualTo("CN=cash.app")
}
@Test
fun organizationalUnit() {
val heldCertificate = HeldCertificate.Builder()
.commonName("cash.app")
.organizationalUnit("cash")
.build()
val certificate = heldCertificate.certificate
assertThat(certificate.getSubjectX500Principal().name).isEqualTo(
"CN=cash.app,OU=cash"
)
}
/** Confirm golden values of encoded PEMs. */
@Test
fun pems() {
val keyFactory = KeyFactory.getInstance("RSA")
val publicKeyBytes = ("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCApFHhtrLan28q+oMolZuaTfWBA0V5aM" +
"Ivq32BsloQu6LlvX1wJ4YEoUCjDlPOtpht7XLbUmBnbIzN89XK4UJVM6Sqp3K88Km8z7gMrdrfTom/274wL25fICR+" +
"yDEQ5fUVYBmJAKXZF1aoI0mIoEx0xFsQhIJ637v2MxJDupd61wIDAQAB")
.decodeBase64()!!
val publicKey = keyFactory.generatePublic(
X509EncodedKeySpec(publicKeyBytes.toByteArray())
)
val privateKeyBytes = ("MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAICkUeG2stqfbyr6gyiVm" +
"5pN9YEDRXlowi+rfYGyWhC7ouW9fXAnhgShQKMOU862mG3tcttSYGdsjM3z1crhQlUzpKqncrzwqbzPuAyt2t9Oib/" +
"bvjAvbl8gJH7IMRDl9RVgGYkApdkXVqgjSYigTHTEWxCEgnrfu/YzEkO6l3rXAgMBAAECgYB99mhnB6piADOuddXv6" +
"26NzUBTr4xbsYRTgSxHzwf50oFTTBSDuW+1IOBVyTWu94SSPyt0LllPbC8Di3sQSTnVGpSqAvEXknBMzIc0UO74Rn9" +
"p3gZjEenPt1l77fIBa2nK06/rdsJCoE/1P1JSfM9w7LU1RsTmseYMLeJl5F79gQJBAO/BbAKqg1yzK7VijygvBoUrr" +
"+rt2lbmKgcUQ/rxu8IIQk0M/xgJqSkXDXuOnboGM7sQSKfJAZUtT7xozvLzV7ECQQCJW59w7NIM0qZ/gIX2gcNZr1B" +
"/V3zcGlolTDciRm+fnKGNt2EEDKnVL3swzbEfTCa48IT0QKgZJqpXZERa26UHAkBLXmiP5f5pk8F3wcXzAeVw06z3k" +
"1IB41Tu6MX+CyPU+TeudRlz+wV8b0zDvK+EnRKCCbptVFj1Bkt8lQ4JfcnhAkAk2Y3Gz+HySrkcT7Cg12M/NkdUQnZ" +
"e3jr88pt/+IGNwomc6Wt/mJ4fcWONTkGMcfOZff1NQeNXDAZ6941XCsIVAkASOg02PlVHLidU7mIE65swMM5/RNhS4" +
"aFjez/MwxFNOHaxc9VgCwYPXCLOtdf7AVovdyG0XWgbUXH+NyxKwboE").decodeBase64()!!
val privateKey = keyFactory.generatePrivate(
PKCS8EncodedKeySpec(privateKeyBytes.toByteArray())
)
val heldCertificate = HeldCertificate.Builder()
.keyPair(publicKey, privateKey)
.commonName("cash.app")
.validityInterval(0L, 1000L)
.rsa2048()
.build()
assertThat(
"""
|-----BEGIN CERTIFICATE-----
|MIIBmjCCAQOgAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDDAhjYXNo
|LmFwcDAeFw03MDAxMDEwMDAwMDBaFw03MDAxMDEwMDAwMDFaMBMxETAPBgNVBAMM
|CGNhc2guYXBwMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCApFHhtrLan28q
|+oMolZuaTfWBA0V5aMIvq32BsloQu6LlvX1wJ4YEoUCjDlPOtpht7XLbUmBnbIzN
|89XK4UJVM6Sqp3K88Km8z7gMrdrfTom/274wL25fICR+yDEQ5fUVYBmJAKXZF1ao
|I0mIoEx0xFsQhIJ637v2MxJDupd61wIDAQABMA0GCSqGSIb3DQEBCwUAA4GBADHT
|vcjwl9Z4I5Cb2R1y7aaa860HkY2k3ThaDK5OJt6GYqJTA9P3LtX7VwQtL1TWqXGc
|+OEfl3zhm0PUqcbckMzhJtqIa7NkDSjNm71BKd843pIhGcEri69DcL/cR8T+eMex
|hadh7aGM9OjeL8gznLeq27Ly6Dj7Vkp5OmOrSKfn
|-----END CERTIFICATE-----
|""".trimMargin()
).isEqualTo(heldCertificate.certificatePem())
assertThat(
"""
|-----BEGIN RSA PRIVATE KEY-----
|MIICWwIBAAKBgQCApFHhtrLan28q+oMolZuaTfWBA0V5aMIvq32BsloQu6LlvX1w
|J4YEoUCjDlPOtpht7XLbUmBnbIzN89XK4UJVM6Sqp3K88Km8z7gMrdrfTom/274w
|L25fICR+yDEQ5fUVYBmJAKXZF1aoI0mIoEx0xFsQhIJ637v2MxJDupd61wIDAQAB
|AoGAffZoZweqYgAzrnXV7+tujc1AU6+MW7GEU4EsR88H+dKBU0wUg7lvtSDgVck1
|rveEkj8rdC5ZT2wvA4t7EEk51RqUqgLxF5JwTMyHNFDu+EZ/ad4GYxHpz7dZe+3y
|AWtpytOv63bCQqBP9T9SUnzPcOy1NUbE5rHmDC3iZeRe/YECQQDvwWwCqoNcsyu1
|Yo8oLwaFK6/q7dpW5ioHFEP68bvCCEJNDP8YCakpFw17jp26BjO7EEinyQGVLU+8
|aM7y81exAkEAiVufcOzSDNKmf4CF9oHDWa9Qf1d83BpaJUw3IkZvn5yhjbdhBAyp
|1S97MM2xH0wmuPCE9ECoGSaqV2REWtulBwJAS15oj+X+aZPBd8HF8wHlcNOs95NS
|AeNU7ujF/gsj1Pk3rnUZc/sFfG9Mw7yvhJ0Sggm6bVRY9QZLfJUOCX3J4QJAJNmN
|xs/h8kq5HE+woNdjPzZHVEJ2Xt46/PKbf/iBjcKJnOlrf5ieH3FjjU5BjHHzmX39
|TUHjVwwGeveNVwrCFQJAEjoNNj5VRy4nVO5iBOubMDDOf0TYUuGhY3s/zMMRTTh2
|sXPVYAsGD1wizrXX+wFaL3chtF1oG1Fx/jcsSsG6BA==
|-----END RSA PRIVATE KEY-----
|""".trimMargin()
).isEqualTo(heldCertificate.privateKeyPkcs1Pem())
assertThat(
"""
|-----BEGIN PRIVATE KEY-----
|MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAICkUeG2stqfbyr6
|gyiVm5pN9YEDRXlowi+rfYGyWhC7ouW9fXAnhgShQKMOU862mG3tcttSYGdsjM3z
|1crhQlUzpKqncrzwqbzPuAyt2t9Oib/bvjAvbl8gJH7IMRDl9RVgGYkApdkXVqgj
|SYigTHTEWxCEgnrfu/YzEkO6l3rXAgMBAAECgYB99mhnB6piADOuddXv626NzUBT
|r4xbsYRTgSxHzwf50oFTTBSDuW+1IOBVyTWu94SSPyt0LllPbC8Di3sQSTnVGpSq
|AvEXknBMzIc0UO74Rn9p3gZjEenPt1l77fIBa2nK06/rdsJCoE/1P1JSfM9w7LU1
|RsTmseYMLeJl5F79gQJBAO/BbAKqg1yzK7VijygvBoUrr+rt2lbmKgcUQ/rxu8II
|Qk0M/xgJqSkXDXuOnboGM7sQSKfJAZUtT7xozvLzV7ECQQCJW59w7NIM0qZ/gIX2
|gcNZr1B/V3zcGlolTDciRm+fnKGNt2EEDKnVL3swzbEfTCa48IT0QKgZJqpXZERa
|26UHAkBLXmiP5f5pk8F3wcXzAeVw06z3k1IB41Tu6MX+CyPU+TeudRlz+wV8b0zD
|vK+EnRKCCbptVFj1Bkt8lQ4JfcnhAkAk2Y3Gz+HySrkcT7Cg12M/NkdUQnZe3jr8
|8pt/+IGNwomc6Wt/mJ4fcWONTkGMcfOZff1NQeNXDAZ6941XCsIVAkASOg02PlVH
|LidU7mIE65swMM5/RNhS4aFjez/MwxFNOHaxc9VgCwYPXCLOtdf7AVovdyG0XWgb
|UXH+NyxKwboE
|-----END PRIVATE KEY-----
|""".trimMargin()
).isEqualTo(heldCertificate.privateKeyPkcs8Pem())
}
@Test
fun ecdsaSignedByRsa() {
val root = HeldCertificate.Builder()
.certificateAuthority(0)
.rsa2048()
.build()
val leaf = HeldCertificate.Builder()
.certificateAuthority(0)
.ecdsa256()
.signedBy(root)
.build()
assertThat(root.certificate.sigAlgName).isEqualToIgnoringCase("SHA256WITHRSA")
assertThat(leaf.certificate.sigAlgName).isEqualToIgnoringCase("SHA256WITHRSA")
}
@Test
fun rsaSignedByEcdsa() {
val root = HeldCertificate.Builder()
.certificateAuthority(0)
.ecdsa256()
.build()
val leaf = HeldCertificate.Builder()
.certificateAuthority(0)
.rsa2048()
.signedBy(root)
.build()
assertThat(root.certificate.sigAlgName).isEqualToIgnoringCase("SHA256WITHECDSA")
assertThat(leaf.certificate.sigAlgName).isEqualToIgnoringCase("SHA256WITHECDSA")
}
@Test
fun decodeEcdsa256() {
// The certificate + private key below was generated programmatically:
//
// HeldCertificate heldCertificate = new HeldCertificate.Builder()
// .validityInterval(5_000L, 10_000L)
// .addSubjectAlternativeName("1.1.1.1")
// .addSubjectAlternativeName("cash.app")
// .serialNumber(42L)
// .commonName("cash.app")
// .organizationalUnit("engineering")
// .ecdsa256()
// .build();
val certificatePem = """
|-----BEGIN CERTIFICATE-----
|MIIBYTCCAQegAwIBAgIBKjAKBggqhkjOPQQDAjApMRQwEgYDVQQLEwtlbmdpbmVl
|cmluZzERMA8GA1UEAxMIY2FzaC5hcHAwHhcNNzAwMTAxMDAwMDA1WhcNNzAwMTAx
|MDAwMDEwWjApMRQwEgYDVQQLEwtlbmdpbmVlcmluZzERMA8GA1UEAxMIY2FzaC5h
|cHAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASda8ChkQXxGELnrV/oBnIAx3dD
|ocUOJfdz4pOJTP6dVQB9U3UBiW5uSX/MoOD0LL5zG3bVyL3Y6pDwKuYvfLNhoyAw
|HjAcBgNVHREBAf8EEjAQhwQBAQEBgghjYXNoLmFwcDAKBggqhkjOPQQDAgNIADBF
|AiAyHHg1N6YDDQiY920+cnI5XSZwEGhAtb9PYWO8bLmkcQIhAI2CfEZf3V/obmdT
|yyaoEufLKVXhrTQhRfodTeigi4RX
|-----END CERTIFICATE-----
|""".trimMargin()
val pkcs8Pem = """
|-----BEGIN PRIVATE KEY-----
|MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCA7ODT0xhGSNn4ESj6J
|lu/GJQZoU9lDrCPeUcQ28tzOWw==
|-----END PRIVATE KEY-----
|""".trimMargin()
val bcPkcs8Pem = """
|-----BEGIN PRIVATE KEY-----
|ME0CAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEMzAxAgEBBCA7ODT0xhGSNn4ESj6J
|lu/GJQZoU9lDrCPeUcQ28tzOW6AKBggqhkjOPQMBBw==
|-----END PRIVATE KEY-----
|""".trimMargin()
val heldCertificate = decode(certificatePem + pkcs8Pem)
assertThat(heldCertificate.certificatePem()).isEqualTo(certificatePem)
// Slightly different encoding
if (platform.isBouncyCastle()) {
assertThat(heldCertificate.privateKeyPkcs8Pem()).isEqualTo(bcPkcs8Pem)
} else {
assertThat(heldCertificate.privateKeyPkcs8Pem()).isEqualTo(pkcs8Pem)
}
val certificate = heldCertificate.certificate
assertThat(certificate.notBefore.time).isEqualTo(5000L)
assertThat(certificate.notAfter.time).isEqualTo(10000L)
assertThat(certificate.subjectAlternativeNames).containsExactly(
listOf(GeneralName.iPAddress, "1.1.1.1"),
listOf(GeneralName.dNSName, "cash.app")
)
assertThat(certificate.getSubjectX500Principal().name)
.isEqualTo("CN=cash.app,OU=engineering")
}
@Test
fun decodeRsa512() {
// The certificate + private key below was generated with OpenSSL. Never generate certificates
// with MD5 or 512-bit RSA; that's insecure!
//
// openssl req \
// -x509 \
// -md5 \
// -nodes \
// -days 1 \
// -newkey rsa:512 \
// -keyout privateKey.key \
// -out certificate.crt
val certificatePem = """
|-----BEGIN CERTIFICATE-----
|MIIBFzCBwgIJAIVAqagcVN7/MA0GCSqGSIb3DQEBBAUAMBMxETAPBgNVBAMMCGNh
|c2guYXBwMB4XDTE5MDkwNzAyMjg0NFoXDTE5MDkwODAyMjg0NFowEzERMA8GA1UE
|AwwIY2FzaC5hcHAwXDANBgkqhkiG9w0BAQEFAANLADBIAkEA8qAeoubm4mBTD9/J
|ujLQkfk/fuJt/T5pVQ1vUEqxfcMw0zYgszQ5C2MiIl7M6JkTRKU01q9hVFCR83wX
|zIdrLQIDAQABMA0GCSqGSIb3DQEBBAUAA0EAO1UpwhrkW3Ho1nZK/taoUQOoqz/n
|HFVMtyEkm5gBDgz8nJXwb3zbegclQyH+kVou02S8zC5WWzEtd0R8S0LsTA==
|-----END CERTIFICATE-----
|""".trimMargin()
val pkcs8Pem = """
|-----BEGIN PRIVATE KEY-----
|MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEA8qAeoubm4mBTD9/J
|ujLQkfk/fuJt/T5pVQ1vUEqxfcMw0zYgszQ5C2MiIl7M6JkTRKU01q9hVFCR83wX
|zIdrLQIDAQABAkEA7dEA9o/5k77y68ZhRv9z7QEwucBcKzQ3rsSCbWMpYqg924F9
|L8Z76kzSedSO2PN8mg6y/OLL+qBuTeUK/yiowQIhAP0cknFMbqeNX6uvj/S+V7in
|bIhQkhcSdJjRw8fxMnJpAiEA9WTp9wzJpn+9etZo0jJ8wkM0+LTMNELo47Ctz7l1
|kiUCIQCi34vslD5wWyzBEcwUtZdFH5dbcF1Rs3KMFA9jzfWkYQIgHtiWiFV1K5a3
|DK/S8UkjYY/tIq4nVRJsD+LvlkLrwnkCIECcz4yF4HQgv+Tbzj/gGSBl1VIliTcB
|Rc5RUQ0mZJQF
|-----END PRIVATE KEY-----
|""".trimMargin()
val heldCertificate = decode(pkcs8Pem + certificatePem)
assertThat(heldCertificate.certificatePem()).isEqualTo(certificatePem)
assertThat(heldCertificate.privateKeyPkcs8Pem()).isEqualTo(pkcs8Pem)
val certificate = heldCertificate.certificate
assertThat(certificate.getSubjectX500Principal().name)
.isEqualTo("CN=cash.app")
}
@Test
fun decodeRsa2048() {
// The certificate + private key below was generated programmatically:
//
// HeldCertificate heldCertificate = new HeldCertificate.Builder()
// .validityInterval(5_000L, 10_000L)
// .addSubjectAlternativeName("1.1.1.1")
// .addSubjectAlternativeName("cash.app")
// .serialNumber(42L)
// .commonName("cash.app")
// .organizationalUnit("engineering")
// .rsa2048()
// .build();
val certificatePem = """
|-----BEGIN CERTIFICATE-----
|MIIC7TCCAdWgAwIBAgIBKjANBgkqhkiG9w0BAQsFADApMRQwEgYDVQQLEwtlbmdp
|bmVlcmluZzERMA8GA1UEAxMIY2FzaC5hcHAwHhcNNzAwMTAxMDAwMDA1WhcNNzAw
|MTAxMDAwMDEwWjApMRQwEgYDVQQLEwtlbmdpbmVlcmluZzERMA8GA1UEAxMIY2Fz
|aC5hcHAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCaU+vrUPL0APGI
|SXIuRX4xRrigXmGKx+GRPnWDWvGJwOm23Vpq/eZxQx6PbSUB1+QZzAwge20RpNAp
|2lt5/qFtgUpEon2j06rd/0+ODqqVJX+6d3SpmF1fPfKUB6AOZbxEkaJpBSTavoTg
|G2M/NMdjZjrcB3quNQcLg54mmI3HJm1zOd/8i2fZjvoiyVY30Inn2SmQsAotXw1u
|aE/319bnR2sQlnkp6MJU0eLEtKyRif/IODvY+mtRYYdkFtoeT6qQPMIh+gF/H3to
|5tjs3g59QC8k2TJDop4EFYUOwdrtnb8wUiBnLyURD1szASE2IO2Ftk1zaNOPKtrv
|VeJuB/mpAgMBAAGjIDAeMBwGA1UdEQEB/wQSMBCHBAEBAQGCCGNhc2guYXBwMA0G
|CSqGSIb3DQEBCwUAA4IBAQAPm7vfk+rxSucxxbFiimmFKBw+ymieLY/kznNh0lHJ
|q15fsMYK7TTTt2FFqyfVXhhRZegLrkrGb3+4Dz1uNtcRrjT4qo+T/JOuZGxtBLbm
|4/hkFSYavtd2FW+/CK7EnQKUyabgLOblb21IHOlcPwpSe6KkJjpwq0TV/ozzfk/q
|kGRA7/Ubn5TMRYyHWnod2SS14+BkItcWN03Z7kvyMYrpNZpu6vQRYsqJJFMcmpGZ
|sZQW31gO2arPmfNotkQdFdNL12c9YZKkJGhyK6NcpffD2l6O9NS5SRD5RnkvBxQw
|fX5DamL8je/YKSLQ4wgUA/5iVKlCiJGQi6fYIJ0kxayO
|-----END CERTIFICATE-----
|""".trimMargin()
val pkcs8Pem = """
|-----BEGIN PRIVATE KEY-----
|MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCaU+vrUPL0APGI
|SXIuRX4xRrigXmGKx+GRPnWDWvGJwOm23Vpq/eZxQx6PbSUB1+QZzAwge20RpNAp
|2lt5/qFtgUpEon2j06rd/0+ODqqVJX+6d3SpmF1fPfKUB6AOZbxEkaJpBSTavoTg
|G2M/NMdjZjrcB3quNQcLg54mmI3HJm1zOd/8i2fZjvoiyVY30Inn2SmQsAotXw1u
|aE/319bnR2sQlnkp6MJU0eLEtKyRif/IODvY+mtRYYdkFtoeT6qQPMIh+gF/H3to
|5tjs3g59QC8k2TJDop4EFYUOwdrtnb8wUiBnLyURD1szASE2IO2Ftk1zaNOPKtrv
|VeJuB/mpAgMBAAECggEAOlOXaYNZn1Cv+INRrR1EmVkSNEIXeX0bymohvbhka1zG
|t/8myiMVsh7c8PYeM3kl034j4y7ixPVWW0sUoaHT3vArYo9LDtzTyj1REu6GGAJp
|KM82/1X/jBx8jufm3SokIoIsMKbqC+ZPj+ep9dx7sxyTCE+nVSnjdL2Uyx+DDg3o
|na237HTScWIi+tMv5QGEwqLHS2q+NZYfjgnSxNY8BRw4XZCcIZRko9niuB5gUjj/
|y01HwvOCWuOMaSKZak1OdOaz3427/TkhYIqf6ft0ELF+ASRk3BLQA06pRt88H3u2
|3vsHJsWr2rkCN0h9uDp2o50ZQ5fvlxqG0QIZmvkIkQKBgQDOHeZKvXO5IxQ+S8ed
|09bC5SKiclCdW+Ry7N2x1MBfrxc4TTTTNaUN9Qdc6RXANG9bX2CJv0Dkh/0yH3z9
|Bdq6YcoP6DFCX46jwhCKvxMX9h9PFLvY7l2VSe7NfboGzvYLCy8ErsGuio8u9MHZ
|osX2ch6Gdhn1xUwLCw+T7rNwjQKBgQC/rWb0sWfgbKhEqV+u5oov+fFjooWmTQlQ
|jcj+lMWUOkitnPmX9TsH5JDa8I89Y0gJGu7Lfg8XSH+4FCCfX3mSLYwVH5vAIvmr
|TjMqRwSahQuTr/g+lx7alpcUHYv3z6b3WYIXFPPr3t7grWNJ14wMv9DnItWOg84H
|LlxAvXXsjQKBgQCRPPhdignVVyaYjwVl7TPTuWoiVbMAbxQW91lwSZ4UzmfqQF0M
|xyw7HYHGsmelPE2LcTWxWpb7cee0PgPwtwNdejLL6q1rO7JjKghF/EYUCFYff1iu
|j6hZ3fLr0cAXtBYjygmjnxDTUMd8KvO9y7j644cm8GlyiUgAMBcWAolmsQKBgQCT
|AJQTWfPGxM6QSi3d32VfwhsFROGnVzGrm/HofYTCV6jhraAmkKcDOKJ3p0LT286l
|XQiC/FzqiGmbbaRPVlPQbiofESzMQIamgMTwyaKYNy1XyP9kUVYSYqfff4GXPqRY
|00bYGPOxlC3utkuNmEgKhxnaCncqY5+hFkceR6+nCQKBgQC1Gonjhw0lYe43aHpp
|nDJKv3FnyN3wxjsR2c9sWpDzHA6CMVhSeLoXCB9ishmrSE/CygNlTU1TEy63xN22
|+dMHl5I/urMesjKKWiKZHdbWVIjJDv25r3jrN9VLr4q6AD9r1Su5G0o2j0N5ujVg
|SzpFHp+ZzhL/SANa8EqlcF6ItQ==
|-----END PRIVATE KEY-----
|""".trimMargin()
val heldCertificate = decode(pkcs8Pem + certificatePem)
assertThat(heldCertificate.certificatePem()).isEqualTo(certificatePem)
assertThat(heldCertificate.privateKeyPkcs8Pem()).isEqualTo(pkcs8Pem)
val certificate = heldCertificate.certificate
assertThat(certificate.notBefore.time).isEqualTo(5000L)
assertThat(certificate.notAfter.time).isEqualTo(10000L)
assertThat(certificate.subjectAlternativeNames).containsExactly(
Arrays.asList(GeneralName.iPAddress, "1.1.1.1"),
Arrays.asList(GeneralName.dNSName, "cash.app")
)
assertThat(certificate.getSubjectX500Principal().name)
.isEqualTo("CN=cash.app,OU=engineering")
}
@Test
fun decodeWrongNumber() {
val certificatePem = """
|-----BEGIN CERTIFICATE-----
|MIIBYTCCAQegAwIBAgIBKjAKBggqhkjOPQQDAjApMRQwEgYDVQQLEwtlbmdpbmVl
|cmluZzERMA8GA1UEAxMIY2FzaC5hcHAwHhcNNzAwMTAxMDAwMDA1WhcNNzAwMTAx
|MDAwMDEwWjApMRQwEgYDVQQLEwtlbmdpbmVlcmluZzERMA8GA1UEAxMIY2FzaC5h
|cHAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASda8ChkQXxGELnrV/oBnIAx3dD
|ocUOJfdz4pOJTP6dVQB9U3UBiW5uSX/MoOD0LL5zG3bVyL3Y6pDwKuYvfLNhoyAw
|HjAcBgNVHREBAf8EEjAQhwQBAQEBgghjYXNoLmFwcDAKBggqhkjOPQQDAgNIADBF
|AiAyHHg1N6YDDQiY920+cnI5XSZwEGhAtb9PYWO8bLmkcQIhAI2CfEZf3V/obmdT
|yyaoEufLKVXhrTQhRfodTeigi4RX
|-----END CERTIFICATE-----
|""".trimMargin()
val pkcs8Pem = """
|-----BEGIN PRIVATE KEY-----
|MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCA7ODT0xhGSNn4ESj6J
|lu/GJQZoU9lDrCPeUcQ28tzOWw==
|-----END PRIVATE KEY-----
|""".trimMargin()
try {
decode(certificatePem)
fail<Any>()
} catch (expected: IllegalArgumentException) {
assertThat(expected).hasMessage("string does not include a private key")
}
try {
decode(pkcs8Pem)
fail<Any>()
} catch (expected: IllegalArgumentException) {
assertThat(expected).hasMessage("string does not include a certificate")
}
try {
decode(certificatePem + pkcs8Pem + certificatePem)
fail<Any>()
} catch (expected: IllegalArgumentException) {
assertThat(expected).hasMessage("string includes multiple certificates")
}
try {
decode(pkcs8Pem + certificatePem + pkcs8Pem)
fail<Any>()
} catch (expected: IllegalArgumentException) {
assertThat(expected).hasMessage("string includes multiple private keys")
}
}
@Test
fun decodeWrongType() {
try {
decode(
"""
|-----BEGIN CERTIFICATE-----
|MIIBmjCCAQOgAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhjYXNo
|-----END CERTIFICATE-----
|-----BEGIN RSA PRIVATE KEY-----
|sXPVYAsGD1wizrXX+wFaL3chtF1oG1Fx/jcsSsG6BA==
|-----END RSA PRIVATE KEY-----
|""".trimMargin()
)
fail<Any>()
} catch (expected: IllegalArgumentException) {
assertThat(expected).hasMessage("unexpected type: RSA PRIVATE KEY")
}
}
@Test
fun decodeMalformed() {
try {
decode(
"""
|-----BEGIN CERTIFICATE-----
|MIIBYTCCAQegAwIBAgIBKjAKBggqhkjOPQQDAjApMRQwEgYDVQQLEwtlbmdpbmVl
|-----END CERTIFICATE-----
|-----BEGIN PRIVATE KEY-----
|MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCA7ODT0xhGSNn4ESj6J
|lu/GJQZoU9lDrCPeUcQ28tzOWw==
|-----END PRIVATE KEY-----
|""".trimMargin()
)
fail<Any>()
} catch (expected: IllegalArgumentException) {
if (!platform.isConscrypt()) {
assertThat(expected).hasMessage("failed to decode certificate")
}
}
try {
decode(
"""
|-----BEGIN CERTIFICATE-----
|MIIBYTCCAQegAwIBAgIBKjAKBggqhkjOPQQDAjApMRQwEgYDVQQLEwtlbmdpbmVl
|cmluZzERMA8GA1UEAxMIY2FzaC5hcHAwHhcNNzAwMTAxMDAwMDA1WhcNNzAwMTAx
|MDAwMDEwWjApMRQwEgYDVQQLEwtlbmdpbmVlcmluZzERMA8GA1UEAxMIY2FzaC5h
|cHAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASda8ChkQXxGELnrV/oBnIAx3dD
|ocUOJfdz4pOJTP6dVQB9U3UBiW5uSX/MoOD0LL5zG3bVyL3Y6pDwKuYvfLNhoyAw
|HjAcBgNVHREBAf8EEjAQhwQBAQEBgghjYXNoLmFwcDAKBggqhkjOPQQDAgNIADBF
|AiAyHHg1N6YDDQiY920+cnI5XSZwEGhAtb9PYWO8bLmkcQIhAI2CfEZf3V/obmdT
|yyaoEufLKVXhrTQhRfodTeigi4RX
|-----END CERTIFICATE-----
|-----BEGIN PRIVATE KEY-----
|MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCA7ODT0xhGSNn4ESj6J
|-----END PRIVATE KEY-----
|""".trimMargin()
)
fail<Any>()
} catch (expected: IllegalArgumentException) {
if (!platform.isConscrypt()) {
assertThat(expected).hasMessage("failed to decode private key")
}
}
}
}

View File

@ -750,8 +750,8 @@ open class CallTest {
val firstResponse = callback.await(request.url).assertSuccessful()
val secondResponse = callback.await(request.url).assertSuccessful()
val bodies: MutableSet<String?> = LinkedHashSet()
bodies.add(firstResponse.getBody())
bodies.add(secondResponse.getBody())
bodies.add(firstResponse.body)
bodies.add(secondResponse.body)
assertThat(bodies).contains("abc")
assertThat(bodies).contains("def")
}

View File

@ -1,222 +0,0 @@
/*
* Copyright (C) 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3;
import org.junit.jupiter.api.Test;
import static okhttp3.CipherSuite.TLS_KRB5_WITH_DES_CBC_MD5;
import static okhttp3.CipherSuite.TLS_RSA_EXPORT_WITH_RC4_40_MD5;
import static okhttp3.CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA256;
import static okhttp3.CipherSuite.forJavaName;
import static okhttp3.internal.Internal.applyConnectionSpec;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.fail;
public class CipherSuiteTest {
@Test public void nullCipherName() {
try {
forJavaName(null);
fail("Should have thrown");
} catch (NullPointerException expected) {
}
}
@Test public void hashCode_usesIdentityHashCode_legacyCase() {
CipherSuite cs = TLS_RSA_EXPORT_WITH_RC4_40_MD5; // This one's javaName starts with "SSL_".
assertThat(cs.hashCode()).overridingErrorMessage(cs.toString()).isEqualTo(
System.identityHashCode(cs));
}
@Test public void hashCode_usesIdentityHashCode_regularCase() {
CipherSuite cs = TLS_RSA_WITH_AES_128_CBC_SHA256; // This one's javaName matches the identifier.
assertThat(cs.hashCode()).overridingErrorMessage(cs.toString()).isEqualTo(
System.identityHashCode(cs));
}
@Test public void instancesAreInterned() {
assertThat(forJavaName("TestCipherSuite")).isSameAs(forJavaName("TestCipherSuite"));
assertThat(forJavaName(TLS_KRB5_WITH_DES_CBC_MD5.javaName()))
.isSameAs(TLS_KRB5_WITH_DES_CBC_MD5);
}
/**
* Tests that interned CipherSuite instances remain the case across garbage collections, even if
* the String used to construct them is no longer strongly referenced outside of the CipherSuite.
*/
@SuppressWarnings("RedundantStringConstructorCall")
@Test public void instancesAreInterned_survivesGarbageCollection() {
// We're not holding onto a reference to this String instance outside of the CipherSuite...
CipherSuite cs = forJavaName(new String("FakeCipherSuite_instancesAreInterned"));
System.gc(); // Unless cs references the String instance, it may now be garbage collected.
assertThat(forJavaName(new String(cs.javaName()))).isSameAs(cs);
}
@Test public void equals() {
assertThat(forJavaName("cipher")).isEqualTo(forJavaName("cipher"));
assertThat(forJavaName("cipherB")).isNotEqualTo(forJavaName("cipherA"));
assertThat(TLS_RSA_EXPORT_WITH_RC4_40_MD5).isEqualTo(
forJavaName("SSL_RSA_EXPORT_WITH_RC4_40_MD5"));
assertThat(TLS_RSA_WITH_AES_128_CBC_SHA256).isNotEqualTo(
TLS_RSA_EXPORT_WITH_RC4_40_MD5);
}
@Test public void forJavaName_acceptsArbitraryStrings() {
// Shouldn't throw.
forJavaName("example CipherSuite name that is not in the whitelist");
}
@Test public void javaName_examples() {
assertThat(TLS_RSA_EXPORT_WITH_RC4_40_MD5.javaName()).isEqualTo(
"SSL_RSA_EXPORT_WITH_RC4_40_MD5");
assertThat(TLS_RSA_WITH_AES_128_CBC_SHA256.javaName()).isEqualTo(
"TLS_RSA_WITH_AES_128_CBC_SHA256");
assertThat(forJavaName("TestCipherSuite").javaName()).isEqualTo("TestCipherSuite");
}
@Test public void javaName_equalsToString() {
assertThat(TLS_RSA_EXPORT_WITH_RC4_40_MD5.toString()).isEqualTo(
TLS_RSA_EXPORT_WITH_RC4_40_MD5.javaName());
assertThat(TLS_RSA_WITH_AES_128_CBC_SHA256.toString()).isEqualTo(
TLS_RSA_WITH_AES_128_CBC_SHA256.javaName());
}
/**
* On the Oracle JVM some older cipher suites have the "SSL_" prefix and others have the "TLS_"
* prefix. On the IBM JVM all cipher suites have the "SSL_" prefix.
*
* <p>Prior to OkHttp 3.3.1 we accepted either form and consider them equivalent. And since OkHttp
* 3.7.0 this is also true. But OkHttp 3.3.1 through 3.6.0 treated these as different.
*/
@Test public void forJavaName_fromLegacyEnumName() {
// These would have been considered equal in OkHttp 3.3.1, but now aren't.
assertThat(forJavaName("SSL_RSA_EXPORT_WITH_RC4_40_MD5")).isEqualTo(
forJavaName("TLS_RSA_EXPORT_WITH_RC4_40_MD5"));
assertThat(forJavaName("SSL_DH_RSA_EXPORT_WITH_DES40_CBC_SHA")).isEqualTo(
forJavaName("TLS_DH_RSA_EXPORT_WITH_DES40_CBC_SHA"));
assertThat(forJavaName("SSL_FAKE_NEW_CIPHER")).isEqualTo(
forJavaName("TLS_FAKE_NEW_CIPHER"));
}
@Test public void applyIntersectionRetainsTlsPrefixes() throws Exception {
FakeSslSocket socket = new FakeSslSocket();
socket.setEnabledProtocols(new String[] { "TLSv1" });
socket.setSupportedCipherSuites(new String[] { "SSL_A", "SSL_B", "SSL_C", "SSL_D", "SSL_E" });
socket.setEnabledCipherSuites(new String[] { "SSL_A", "SSL_B", "SSL_C" });
ConnectionSpec connectionSpec = new ConnectionSpec.Builder(true)
.tlsVersions(TlsVersion.TLS_1_0)
.cipherSuites("TLS_A", "TLS_C", "TLS_E")
.build();
applyConnectionSpec(connectionSpec, socket, false);
assertArrayEquals(new String[] { "TLS_A", "TLS_C" }, socket.enabledCipherSuites);
}
@Test public void applyIntersectionRetainsSslPrefixes() throws Exception {
FakeSslSocket socket = new FakeSslSocket();
socket.setEnabledProtocols(new String[] { "TLSv1" });
socket.setSupportedCipherSuites(new String[] { "TLS_A", "TLS_B", "TLS_C", "TLS_D", "TLS_E" });
socket.setEnabledCipherSuites(new String[] { "TLS_A", "TLS_B", "TLS_C" });
ConnectionSpec connectionSpec = new ConnectionSpec.Builder(true)
.tlsVersions(TlsVersion.TLS_1_0)
.cipherSuites("SSL_A", "SSL_C", "SSL_E")
.build();
applyConnectionSpec(connectionSpec, socket, false);
assertArrayEquals(new String[] { "SSL_A", "SSL_C" }, socket.enabledCipherSuites);
}
@Test public void applyIntersectionAddsSslScsvForFallback() throws Exception {
FakeSslSocket socket = new FakeSslSocket();
socket.setEnabledProtocols(new String[] { "TLSv1" });
socket.setSupportedCipherSuites(new String[] { "SSL_A", "SSL_FALLBACK_SCSV" });
socket.setEnabledCipherSuites(new String[] { "SSL_A" });
ConnectionSpec connectionSpec = new ConnectionSpec.Builder(true)
.tlsVersions(TlsVersion.TLS_1_0)
.cipherSuites("SSL_A")
.build();
applyConnectionSpec(connectionSpec, socket, true);
assertArrayEquals(new String[] { "SSL_A", "SSL_FALLBACK_SCSV" }, socket.enabledCipherSuites);
}
@Test public void applyIntersectionAddsTlsScsvForFallback() throws Exception {
FakeSslSocket socket = new FakeSslSocket();
socket.setEnabledProtocols(new String[] { "TLSv1" });
socket.setSupportedCipherSuites(new String[] { "TLS_A", "TLS_FALLBACK_SCSV" });
socket.setEnabledCipherSuites(new String[] { "TLS_A" });
ConnectionSpec connectionSpec = new ConnectionSpec.Builder(true)
.tlsVersions(TlsVersion.TLS_1_0)
.cipherSuites("TLS_A")
.build();
applyConnectionSpec(connectionSpec, socket, true);
assertArrayEquals(new String[] { "TLS_A", "TLS_FALLBACK_SCSV" }, socket.enabledCipherSuites);
}
@Test public void applyIntersectionToProtocolVersion() throws Exception {
FakeSslSocket socket = new FakeSslSocket();
socket.setEnabledProtocols(new String[] { "TLSv1", "TLSv1.1", "TLSv1.2" });
socket.setSupportedCipherSuites(new String[] { "TLS_A" });
socket.setEnabledCipherSuites(new String[] { "TLS_A" });
ConnectionSpec connectionSpec = new ConnectionSpec.Builder(true)
.tlsVersions(TlsVersion.TLS_1_1, TlsVersion.TLS_1_2, TlsVersion.TLS_1_3)
.cipherSuites("TLS_A")
.build();
applyConnectionSpec(connectionSpec, socket, false);
assertArrayEquals(new String[] { "TLSv1.1", "TLSv1.2" }, socket.enabledProtocols);
}
static final class FakeSslSocket extends DelegatingSSLSocket {
private String[] enabledProtocols;
private String[] supportedCipherSuites;
private String[] enabledCipherSuites;
FakeSslSocket() {
super(null);
}
@Override public String[] getEnabledProtocols() {
return enabledProtocols;
}
@Override public void setEnabledProtocols(String[] enabledProtocols) {
this.enabledProtocols = enabledProtocols;
}
@Override public String[] getSupportedCipherSuites() {
return supportedCipherSuites;
}
public void setSupportedCipherSuites(String[] supportedCipherSuites) {
this.supportedCipherSuites = supportedCipherSuites;
}
@Override public String[] getEnabledCipherSuites() {
return enabledCipherSuites;
}
@Override public void setEnabledCipherSuites(String[] enabledCipherSuites) {
this.enabledCipherSuites = enabledCipherSuites;
}
}
}

View File

@ -0,0 +1,220 @@
/*
* Copyright (C) 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3
import okhttp3.CipherSuite.Companion.forJavaName
import okhttp3.internal.applyConnectionSpec
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Assertions.assertArrayEquals
import org.junit.jupiter.api.Test
class CipherSuiteTest {
@Test
fun hashCode_usesIdentityHashCode_legacyCase() {
val cs = CipherSuite.TLS_RSA_EXPORT_WITH_RC4_40_MD5 // This one's javaName starts with "SSL_".
assertThat(cs.hashCode())
.overridingErrorMessage(cs.toString())
.isEqualTo(System.identityHashCode(cs))
}
@Test
fun hashCode_usesIdentityHashCode_regularCase() {
// This one's javaName matches the identifier.
val cs = CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA256
assertThat(cs.hashCode())
.overridingErrorMessage(cs.toString())
.isEqualTo(System.identityHashCode(cs))
}
@Test
fun instancesAreInterned() {
assertThat(forJavaName("TestCipherSuite"))
.isSameAs(forJavaName("TestCipherSuite"))
assertThat(forJavaName(CipherSuite.TLS_KRB5_WITH_DES_CBC_MD5.javaName))
.isSameAs(CipherSuite.TLS_KRB5_WITH_DES_CBC_MD5)
}
/**
* Tests that interned CipherSuite instances remain the case across garbage collections, even if
* the String used to construct them is no longer strongly referenced outside of the CipherSuite.
*/
@Test
fun instancesAreInterned_survivesGarbageCollection() {
// We're not holding onto a reference to this String instance outside of the CipherSuite...
val cs = forJavaName("FakeCipherSuite_instancesAreInterned")
System.gc() // Unless cs references the String instance, it may now be garbage collected.
assertThat(forJavaName(java.lang.String(cs.javaName) as String))
.isSameAs(cs)
}
@Test
fun equals() {
assertThat(forJavaName("cipher")).isEqualTo(forJavaName("cipher"))
assertThat(forJavaName("cipherB")).isNotEqualTo(forJavaName("cipherA"))
assertThat(CipherSuite.TLS_RSA_EXPORT_WITH_RC4_40_MD5)
.isEqualTo(forJavaName("SSL_RSA_EXPORT_WITH_RC4_40_MD5"))
assertThat(CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA256)
.isNotEqualTo(CipherSuite.TLS_RSA_EXPORT_WITH_RC4_40_MD5)
}
@Test
fun forJavaName_acceptsArbitraryStrings() {
// Shouldn't throw.
forJavaName("example CipherSuite name that is not in the whitelist")
}
@Test
fun javaName_examples() {
assertThat(CipherSuite.TLS_RSA_EXPORT_WITH_RC4_40_MD5.javaName)
.isEqualTo("SSL_RSA_EXPORT_WITH_RC4_40_MD5")
assertThat(CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA256.javaName)
.isEqualTo("TLS_RSA_WITH_AES_128_CBC_SHA256")
assertThat(forJavaName("TestCipherSuite").javaName)
.isEqualTo("TestCipherSuite")
}
@Test
fun javaName_equalsToString() {
assertThat(CipherSuite.TLS_RSA_EXPORT_WITH_RC4_40_MD5.toString())
.isEqualTo(CipherSuite.TLS_RSA_EXPORT_WITH_RC4_40_MD5.javaName)
assertThat(CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA256.toString())
.isEqualTo(CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA256.javaName)
}
/**
* On the Oracle JVM some older cipher suites have the "SSL_" prefix and others have the "TLS_"
* prefix. On the IBM JVM all cipher suites have the "SSL_" prefix.
*
* Prior to OkHttp 3.3.1 we accepted either form and consider them equivalent. And since OkHttp
* 3.7.0 this is also true. But OkHttp 3.3.1 through 3.6.0 treated these as different.
*/
@Test
fun forJavaName_fromLegacyEnumName() {
// These would have been considered equal in OkHttp 3.3.1, but now aren't.
assertThat(forJavaName("SSL_RSA_EXPORT_WITH_RC4_40_MD5"))
.isEqualTo(forJavaName("TLS_RSA_EXPORT_WITH_RC4_40_MD5"))
assertThat(forJavaName("SSL_DH_RSA_EXPORT_WITH_DES40_CBC_SHA"))
.isEqualTo(forJavaName("TLS_DH_RSA_EXPORT_WITH_DES40_CBC_SHA"))
assertThat(forJavaName("SSL_FAKE_NEW_CIPHER"))
.isEqualTo(forJavaName("TLS_FAKE_NEW_CIPHER"))
}
@Test
fun applyIntersectionRetainsTlsPrefixes() {
val socket = FakeSslSocket()
socket.enabledProtocols = arrayOf("TLSv1")
socket.supportedCipherSuites = arrayOf("SSL_A", "SSL_B", "SSL_C", "SSL_D", "SSL_E")
socket.enabledCipherSuites = arrayOf("SSL_A", "SSL_B", "SSL_C")
val connectionSpec = ConnectionSpec.Builder(true)
.tlsVersions(TlsVersion.TLS_1_0)
.cipherSuites("TLS_A", "TLS_C", "TLS_E")
.build()
applyConnectionSpec(connectionSpec, socket, false)
assertArrayEquals(arrayOf("TLS_A", "TLS_C"), socket.enabledCipherSuites)
}
@Test
fun applyIntersectionRetainsSslPrefixes() {
val socket = FakeSslSocket()
socket.enabledProtocols = arrayOf("TLSv1")
socket.supportedCipherSuites =
arrayOf("TLS_A", "TLS_B", "TLS_C", "TLS_D", "TLS_E")
socket.enabledCipherSuites = arrayOf("TLS_A", "TLS_B", "TLS_C")
val connectionSpec = ConnectionSpec.Builder(true)
.tlsVersions(TlsVersion.TLS_1_0)
.cipherSuites("SSL_A", "SSL_C", "SSL_E")
.build()
applyConnectionSpec(connectionSpec, socket, false)
assertArrayEquals(arrayOf("SSL_A", "SSL_C"), socket.enabledCipherSuites)
}
@Test
fun applyIntersectionAddsSslScsvForFallback() {
val socket = FakeSslSocket()
socket.enabledProtocols = arrayOf("TLSv1")
socket.supportedCipherSuites = arrayOf("SSL_A", "SSL_FALLBACK_SCSV")
socket.enabledCipherSuites = arrayOf("SSL_A")
val connectionSpec = ConnectionSpec.Builder(true)
.tlsVersions(TlsVersion.TLS_1_0)
.cipherSuites("SSL_A")
.build()
applyConnectionSpec(connectionSpec, socket, true)
assertArrayEquals(
arrayOf("SSL_A", "SSL_FALLBACK_SCSV"),
socket.enabledCipherSuites
)
}
@Test
fun applyIntersectionAddsTlsScsvForFallback() {
val socket = FakeSslSocket()
socket.enabledProtocols = arrayOf("TLSv1")
socket.supportedCipherSuites = arrayOf("TLS_A", "TLS_FALLBACK_SCSV")
socket.enabledCipherSuites = arrayOf("TLS_A")
val connectionSpec = ConnectionSpec.Builder(true)
.tlsVersions(TlsVersion.TLS_1_0)
.cipherSuites("TLS_A")
.build()
applyConnectionSpec(connectionSpec, socket, true)
assertArrayEquals(
arrayOf("TLS_A", "TLS_FALLBACK_SCSV"),
socket.enabledCipherSuites
)
}
@Test
fun applyIntersectionToProtocolVersion() {
val socket = FakeSslSocket()
socket.enabledProtocols = arrayOf("TLSv1", "TLSv1.1", "TLSv1.2")
socket.supportedCipherSuites = arrayOf("TLS_A")
socket.enabledCipherSuites = arrayOf("TLS_A")
val connectionSpec = ConnectionSpec.Builder(true)
.tlsVersions(TlsVersion.TLS_1_1, TlsVersion.TLS_1_2, TlsVersion.TLS_1_3)
.cipherSuites("TLS_A")
.build()
applyConnectionSpec(connectionSpec, socket, false)
assertArrayEquals(arrayOf("TLSv1.1", "TLSv1.2"), socket.enabledProtocols)
}
internal class FakeSslSocket : DelegatingSSLSocket(null) {
private lateinit var enabledProtocols: Array<String>
private lateinit var supportedCipherSuites: Array<String>
private lateinit var enabledCipherSuites: Array<String>
override fun getEnabledProtocols(): Array<String> {
return enabledProtocols
}
override fun setEnabledProtocols(protocols: Array<String>) {
this.enabledProtocols = protocols
}
override fun getSupportedCipherSuites(): Array<String> {
return supportedCipherSuites
}
fun setSupportedCipherSuites(supportedCipherSuites: Array<String>) {
this.supportedCipherSuites = supportedCipherSuites
}
override fun getEnabledCipherSuites(): Array<String> {
return enabledCipherSuites
}
override fun setEnabledCipherSuites(suites: Array<String>) {
this.enabledCipherSuites = suites
}
}
}

View File

@ -1,370 +0,0 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3;
import java.io.IOException;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.HttpCookie;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.URI;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import mockwebserver3.MockResponse;
import mockwebserver3.MockWebServer;
import mockwebserver3.RecordedRequest;
import okhttp3.java.net.cookiejar.JavaNetCookieJar;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.extension.RegisterExtension;
import static java.net.CookiePolicy.ACCEPT_ORIGINAL_SERVER;
import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.data.Offset.offset;
import static org.junit.jupiter.api.Assertions.fail;
/** Derived from Android's CookiesTest. */
@Timeout(30)
public class CookiesTest {
@RegisterExtension public final OkHttpClientTestRule clientTestRule = new OkHttpClientTestRule();
private MockWebServer server;
private OkHttpClient client = clientTestRule.newClient();
@BeforeEach
public void setUp(MockWebServer server) throws Exception {
this.server = server;
}
@Test
public void testNetscapeResponse() throws Exception {
CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER);
client = client.newBuilder()
.cookieJar(new JavaNetCookieJar(cookieManager))
.build();
HttpUrl urlWithIpAddress = urlWithIpAddress(server, "/path/foo");
server.enqueue(new MockResponse.Builder()
.addHeader("Set-Cookie: a=android; "
+ "expires=Fri, 31-Dec-9999 23:59:59 GMT; "
+ "path=/path; "
+ "domain=" + urlWithIpAddress.host() + "; "
+ "secure")
.build());
get(urlWithIpAddress);
List<HttpCookie> cookies = cookieManager.getCookieStore().getCookies();
assertThat(cookies.size()).isEqualTo(1);
HttpCookie cookie = cookies.get(0);
assertThat(cookie.getName()).isEqualTo("a");
assertThat(cookie.getValue()).isEqualTo("android");
assertThat(cookie.getComment()).isNull();
assertThat(cookie.getCommentURL()).isNull();
assertThat(cookie.getDiscard()).isFalse();
assertThat(cookie.getMaxAge()).isGreaterThan(100000000000L);
assertThat(cookie.getPath()).isEqualTo("/path");
assertThat(cookie.getSecure()).isTrue();
assertThat(cookie.getVersion()).isEqualTo(0);
}
@Test public void testRfc2109Response() throws Exception {
CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER);
client = client.newBuilder()
.cookieJar(new JavaNetCookieJar(cookieManager))
.build();
HttpUrl urlWithIpAddress = urlWithIpAddress(server, "/path/foo");
server.enqueue(new MockResponse.Builder()
.addHeader("Set-Cookie: a=android; "
+ "Comment=this cookie is delicious; "
+ "Domain=" + urlWithIpAddress.host() + "; "
+ "Max-Age=60; "
+ "Path=/path; "
+ "Secure; "
+ "Version=1")
.build());
get(urlWithIpAddress);
List<HttpCookie> cookies = cookieManager.getCookieStore().getCookies();
assertThat(cookies.size()).isEqualTo(1);
HttpCookie cookie = cookies.get(0);
assertThat(cookie.getName()).isEqualTo("a");
assertThat(cookie.getValue()).isEqualTo("android");
assertThat(cookie.getCommentURL()).isNull();
assertThat(cookie.getDiscard()).isFalse();
// Converting to a fixed date can cause rounding!
assertThat((double) cookie.getMaxAge()).isCloseTo(60.0, offset(5.0));
assertThat(cookie.getPath()).isEqualTo("/path");
assertThat(cookie.getSecure()).isTrue();
}
@Test public void testQuotedAttributeValues() throws Exception {
CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER);
client = client.newBuilder()
.cookieJar(new JavaNetCookieJar(cookieManager))
.build();
HttpUrl urlWithIpAddress = urlWithIpAddress(server, "/path/foo");
server.enqueue(new MockResponse.Builder()
.addHeader("Set-Cookie: a=\"android\"; "
+ "Comment=\"this cookie is delicious\"; "
+ "CommentURL=\"http://google.com/\"; "
+ "Discard; "
+ "Domain=" + urlWithIpAddress.host() + "; "
+ "Max-Age=60; "
+ "Path=\"/path\"; "
+ "Port=\"80,443," + server.getPort() + "\"; "
+ "Secure; "
+ "Version=\"1\"")
.build());
get(urlWithIpAddress);
List<HttpCookie> cookies = cookieManager.getCookieStore().getCookies();
assertThat(cookies.size()).isEqualTo(1);
HttpCookie cookie = cookies.get(0);
assertThat(cookie.getName()).isEqualTo("a");
assertThat(cookie.getValue()).isEqualTo("android");
// Converting to a fixed date can cause rounding!
assertThat((double) cookie.getMaxAge()).isCloseTo(60.0, offset(1.0));
assertThat(cookie.getPath()).isEqualTo("/path");
assertThat(cookie.getSecure()).isTrue();
}
@Test public void testSendingCookiesFromStore() throws Exception {
server.enqueue(new MockResponse());
HttpUrl serverUrl = urlWithIpAddress(server, "/");
CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER);
HttpCookie cookieA = new HttpCookie("a", "android");
cookieA.setDomain(serverUrl.host());
cookieA.setPath("/");
cookieManager.getCookieStore().add(serverUrl.uri(), cookieA);
HttpCookie cookieB = new HttpCookie("b", "banana");
cookieB.setDomain(serverUrl.host());
cookieB.setPath("/");
cookieManager.getCookieStore().add(serverUrl.uri(), cookieB);
client = client.newBuilder()
.cookieJar(new JavaNetCookieJar(cookieManager))
.build();
get(serverUrl);
RecordedRequest request = server.takeRequest();
assertThat(request.getHeaders().get("Cookie")).isEqualTo("a=android; b=banana");
}
@Test public void cookieHandlerLikeAndroid() throws Exception {
server.enqueue(new MockResponse());
final HttpUrl serverUrl = urlWithIpAddress(server, "/");
CookieHandler androidCookieHandler = new CookieHandler() {
@Override public Map<String, List<String>> get(URI uri, Map<String, List<String>> map) {
return Collections.singletonMap("Cookie", Collections.singletonList("$Version=\"1\"; "
+ "a=\"android\";$Path=\"/\";$Domain=\"" + serverUrl.host() + "\"; "
+ "b=\"banana\";$Path=\"/\";$Domain=\"" + serverUrl.host() + "\""));
}
@Override public void put(URI uri, Map<String, List<String>> map) throws IOException {
}
};
client = client.newBuilder()
.cookieJar(new JavaNetCookieJar(androidCookieHandler))
.build();
get(serverUrl);
RecordedRequest request = server.takeRequest();
assertThat(request.getHeaders().get("Cookie")).isEqualTo("a=android; b=banana");
}
@Test public void receiveAndSendMultipleCookies() throws Exception {
server.enqueue(new MockResponse.Builder()
.addHeader("Set-Cookie", "a=android")
.addHeader("Set-Cookie", "b=banana")
.build());
server.enqueue(new MockResponse());
CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER);
client = client.newBuilder()
.cookieJar(new JavaNetCookieJar(cookieManager))
.build();
get(urlWithIpAddress(server, "/"));
RecordedRequest request1 = server.takeRequest();
assertThat(request1.getHeaders().get("Cookie")).isNull();
get(urlWithIpAddress(server, "/"));
RecordedRequest request2 = server.takeRequest();
assertThat(request2.getHeaders().get("Cookie")).isEqualTo("a=android; b=banana");
}
@Test public void testRedirectsDoNotIncludeTooManyCookies() throws Exception {
MockWebServer redirectTarget = new MockWebServer();
redirectTarget.enqueue(new MockResponse.Builder().body("A").build());
redirectTarget.start();
HttpUrl redirectTargetUrl = urlWithIpAddress(redirectTarget, "/");
MockWebServer redirectSource = new MockWebServer();
redirectSource.enqueue(new MockResponse.Builder()
.code(HttpURLConnection.HTTP_MOVED_TEMP)
.addHeader("Location: " + redirectTargetUrl)
.build());
redirectSource.start();
HttpUrl redirectSourceUrl = urlWithIpAddress(redirectSource, "/");
CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER);
HttpCookie cookie = new HttpCookie("c", "cookie");
cookie.setDomain(redirectSourceUrl.host());
cookie.setPath("/");
String portList = Integer.toString(redirectSource.getPort());
cookie.setPortlist(portList);
cookieManager.getCookieStore().add(redirectSourceUrl.uri(), cookie);
client = client.newBuilder()
.cookieJar(new JavaNetCookieJar(cookieManager))
.build();
get(redirectSourceUrl);
RecordedRequest request = redirectSource.takeRequest();
assertThat(request.getHeaders().get("Cookie")).isEqualTo("c=cookie");
for (String header : redirectTarget.takeRequest().getHeaders().names()) {
if (header.startsWith("Cookie")) {
fail(header);
}
}
}
@Test public void testCookiesSentIgnoresCase() throws Exception {
client = client.newBuilder()
.cookieJar(new JavaNetCookieJar(new CookieManager() {
@Override public Map<String, List<String>> get(URI uri,
Map<String, List<String>> requestHeaders) {
Map<String, List<String>> result = new LinkedHashMap<>();
result.put("COOKIE", Collections.singletonList("Bar=bar"));
result.put("cooKIE2", Collections.singletonList("Baz=baz"));
return result;
}
}))
.build();
server.enqueue(new MockResponse());
get(server.url("/"));
RecordedRequest request = server.takeRequest();
assertThat(request.getHeaders().get("Cookie")).isEqualTo("Bar=bar; Baz=baz");
assertThat(request.getHeaders().get("Cookie2")).isNull();
assertThat(request.getHeaders().get("Quux")).isNull();
}
@Test public void acceptOriginalServerMatchesSubdomain() throws Exception {
CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER);
JavaNetCookieJar cookieJar = new JavaNetCookieJar(cookieManager);
HttpUrl url = HttpUrl.get("https://www.squareup.com/");
cookieJar.saveFromResponse(url, asList(
Cookie.parse(url, "a=android; Domain=squareup.com")));
List<Cookie> actualCookies = cookieJar.loadForRequest(url);
assertThat(actualCookies.size()).isEqualTo(1);
assertThat(actualCookies.get(0).name()).isEqualTo("a");
assertThat(actualCookies.get(0).value()).isEqualTo("android");
}
@Test public void acceptOriginalServerMatchesRfc2965Dot() throws Exception {
CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER);
JavaNetCookieJar cookieJar = new JavaNetCookieJar(cookieManager);
HttpUrl url = HttpUrl.get("https://www.squareup.com/");
cookieJar.saveFromResponse(url, asList(
Cookie.parse(url, "a=android; Domain=.squareup.com")));
List<Cookie> actualCookies = cookieJar.loadForRequest(url);
assertThat(actualCookies.size()).isEqualTo(1);
assertThat(actualCookies.get(0).name()).isEqualTo("a");
assertThat(actualCookies.get(0).value()).isEqualTo("android");
}
@Test public void acceptOriginalServerMatchesExactly() throws Exception {
CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER);
JavaNetCookieJar cookieJar = new JavaNetCookieJar(cookieManager);
HttpUrl url = HttpUrl.get("https://squareup.com/");
cookieJar.saveFromResponse(url, asList(
Cookie.parse(url, "a=android; Domain=squareup.com")));
List<Cookie> actualCookies = cookieJar.loadForRequest(url);
assertThat(actualCookies.size()).isEqualTo(1);
assertThat(actualCookies.get(0).name()).isEqualTo("a");
assertThat(actualCookies.get(0).value()).isEqualTo("android");
}
@Test public void acceptOriginalServerDoesNotMatchDifferentServer() throws Exception {
CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER);
JavaNetCookieJar cookieJar = new JavaNetCookieJar(cookieManager);
HttpUrl url1 = HttpUrl.get("https://api.squareup.com/");
cookieJar.saveFromResponse(url1, asList(
Cookie.parse(url1, "a=android; Domain=api.squareup.com")));
HttpUrl url2 = HttpUrl.get("https://www.squareup.com/");
List<Cookie> actualCookies = cookieJar.loadForRequest(url2);
assertThat(actualCookies).isEmpty();
}
@Test public void testQuoteStripping() throws Exception {
client = client.newBuilder()
.cookieJar(new JavaNetCookieJar(new CookieManager() {
@Override public Map<String, List<String>> get(URI uri,
Map<String, List<String>> requestHeaders) {
Map<String, List<String>> result = new LinkedHashMap<>();
result.put("COOKIE", Collections.singletonList("Bar=\""));
result.put("cooKIE2", Collections.singletonList("Baz=\"baz\""));
return result;
}
}))
.build();
server.enqueue(new MockResponse());
get(server.url("/"));
RecordedRequest request = server.takeRequest();
assertThat(request.getHeaders().get("Cookie")).isEqualTo("Bar=\"; Baz=baz");
assertThat(request.getHeaders().get("Cookie2")).isNull();
assertThat(request.getHeaders().get("Quux")).isNull();
}
private HttpUrl urlWithIpAddress(MockWebServer server, String path) throws Exception {
return server.url(path)
.newBuilder()
.host(InetAddress.getByName(server.getHostName()).getHostAddress())
.build();
}
private void get(HttpUrl url) throws Exception {
Call call = client.newCall(new Request.Builder()
.url(url)
.build());
Response response = call.execute();
response.body().close();
}
}

View File

@ -0,0 +1,353 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3
import java.net.CookieHandler
import java.net.CookieManager
import java.net.CookiePolicy
import java.net.HttpCookie
import java.net.HttpURLConnection
import java.net.InetAddress
import java.net.URI
import mockwebserver3.MockResponse
import mockwebserver3.MockWebServer
import okhttp3.Cookie.Companion.parse
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.java.net.cookiejar.JavaNetCookieJar
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.data.Offset
import org.junit.jupiter.api.Assertions.fail
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Timeout
import org.junit.jupiter.api.extension.RegisterExtension
/** Derived from Android's CookiesTest. */
@Timeout(30)
class CookiesTest {
@RegisterExtension
val clientTestRule = OkHttpClientTestRule()
private lateinit var server: MockWebServer
private var client = clientTestRule.newClient()
@BeforeEach
fun setUp(server: MockWebServer) {
this.server = server
}
@Test
fun testNetscapeResponse() {
val cookieManager = CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER)
client = client.newBuilder()
.cookieJar(JavaNetCookieJar(cookieManager))
.build()
val urlWithIpAddress = urlWithIpAddress(server, "/path/foo")
server.enqueue(
MockResponse.Builder()
.addHeader(
"Set-Cookie: a=android; "
+ "expires=Fri, 31-Dec-9999 23:59:59 GMT; "
+ "path=/path; "
+ "domain=${urlWithIpAddress.host}; "
+ "secure"
)
.build()
)
get(urlWithIpAddress)
val cookies = cookieManager.cookieStore.cookies
assertThat(cookies.size).isEqualTo(1)
val cookie = cookies[0]
assertThat(cookie.name).isEqualTo("a")
assertThat(cookie.value).isEqualTo("android")
assertThat(cookie.comment).isNull()
assertThat(cookie.commentURL).isNull()
assertThat(cookie.discard).isFalse()
assertThat(cookie.maxAge).isGreaterThan(100000000000L)
assertThat(cookie.path).isEqualTo("/path")
assertThat(cookie.secure).isTrue()
assertThat(cookie.version).isEqualTo(0)
}
@Test
fun testRfc2109Response() {
val cookieManager = CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER)
client = client.newBuilder()
.cookieJar(JavaNetCookieJar(cookieManager))
.build()
val urlWithIpAddress = urlWithIpAddress(server, "/path/foo")
server.enqueue(
MockResponse.Builder()
.addHeader(
"Set-Cookie: a=android; "
+ "Comment=this cookie is delicious; "
+ "Domain=${urlWithIpAddress.host}; "
+ "Max-Age=60; "
+ "Path=/path; "
+ "Secure; "
+ "Version=1"
)
.build()
)
get(urlWithIpAddress)
val cookies = cookieManager.cookieStore.cookies
assertThat(cookies.size).isEqualTo(1)
val cookie = cookies[0]
assertThat(cookie.name).isEqualTo("a")
assertThat(cookie.value).isEqualTo("android")
assertThat(cookie.commentURL).isNull()
assertThat(cookie.discard).isFalse()
// Converting to a fixed date can cause rounding!
assertThat(cookie.maxAge.toDouble()).isCloseTo(60.0, Offset.offset(5.0))
assertThat(cookie.path).isEqualTo("/path")
assertThat(cookie.secure).isTrue()
}
@Test
fun testQuotedAttributeValues() {
val cookieManager = CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER)
client = client.newBuilder()
.cookieJar(JavaNetCookieJar(cookieManager))
.build()
val urlWithIpAddress = urlWithIpAddress(server, "/path/foo")
server.enqueue(
MockResponse.Builder()
.addHeader(
"Set-Cookie: a=\"android\"; "
+ "Comment=\"this cookie is delicious\"; "
+ "CommentURL=\"http://google.com/\"; "
+ "Discard; "
+ "Domain=${urlWithIpAddress.host}; "
+ "Max-Age=60; "
+ "Path=\"/path\"; "
+ "Port=\"80,443,${server.port}\"; "
+ "Secure; "
+ "Version=\"1\""
)
.build()
)
get(urlWithIpAddress)
val cookies = cookieManager.cookieStore.cookies
assertThat(cookies.size).isEqualTo(1)
val cookie = cookies[0]
assertThat(cookie.name).isEqualTo("a")
assertThat(cookie.value).isEqualTo("android")
// Converting to a fixed date can cause rounding!
assertThat(cookie.maxAge.toDouble()).isCloseTo(60.0, Offset.offset(1.0))
assertThat(cookie.path).isEqualTo("/path")
assertThat(cookie.secure).isTrue()
}
@Test
fun testSendingCookiesFromStore() {
server.enqueue(MockResponse())
val serverUrl = urlWithIpAddress(server, "/")
val cookieManager = CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER)
val cookieA = HttpCookie("a", "android")
cookieA.domain = serverUrl.host
cookieA.path = "/"
cookieManager.cookieStore.add(serverUrl.toUri(), cookieA)
val cookieB = HttpCookie("b", "banana")
cookieB.domain = serverUrl.host
cookieB.path = "/"
cookieManager.cookieStore.add(serverUrl.toUri(), cookieB)
client = client.newBuilder()
.cookieJar(JavaNetCookieJar(cookieManager))
.build()
get(serverUrl)
val request = server.takeRequest()
assertThat(request.headers["Cookie"]).isEqualTo("a=android; b=banana")
}
@Test
fun cookieHandlerLikeAndroid() {
server.enqueue(MockResponse())
val serverUrl = urlWithIpAddress(server, "/")
val androidCookieHandler: CookieHandler = object : CookieHandler() {
override fun get(uri: URI, map: Map<String, List<String>>) = mapOf(
"Cookie" to listOf(
"\$Version=\"1\"; " +
"a=\"android\";\$Path=\"/\";\$Domain=\"${serverUrl.host}\"; " +
"b=\"banana\";\$Path=\"/\";\$Domain=\"${serverUrl.host}\""
)
)
override fun put(uri: URI, map: Map<String, List<String>>) {
}
}
client = client.newBuilder()
.cookieJar(JavaNetCookieJar(androidCookieHandler))
.build()
get(serverUrl)
val request = server.takeRequest()
assertThat(request.headers["Cookie"]).isEqualTo("a=android; b=banana")
}
@Test
fun receiveAndSendMultipleCookies() {
server.enqueue(
MockResponse.Builder()
.addHeader("Set-Cookie", "a=android")
.addHeader("Set-Cookie", "b=banana")
.build()
)
server.enqueue(MockResponse())
val cookieManager = CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER)
client = client.newBuilder()
.cookieJar(JavaNetCookieJar(cookieManager))
.build()
get(urlWithIpAddress(server, "/"))
val request1 = server.takeRequest()
assertThat(request1.headers["Cookie"]).isNull()
get(urlWithIpAddress(server, "/"))
val request2 = server.takeRequest()
assertThat(request2.headers["Cookie"]).isEqualTo("a=android; b=banana")
}
@Test
fun testRedirectsDoNotIncludeTooManyCookies() {
val redirectTarget = MockWebServer()
redirectTarget.enqueue(MockResponse.Builder().body("A").build())
redirectTarget.start()
val redirectTargetUrl = urlWithIpAddress(redirectTarget, "/")
val redirectSource = MockWebServer()
redirectSource.enqueue(
MockResponse.Builder()
.code(HttpURLConnection.HTTP_MOVED_TEMP)
.addHeader("Location: $redirectTargetUrl")
.build()
)
redirectSource.start()
val redirectSourceUrl = urlWithIpAddress(redirectSource, "/")
val cookieManager = CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER)
val cookie = HttpCookie("c", "cookie")
cookie.domain = redirectSourceUrl.host
cookie.path = "/"
val portList = redirectSource.port.toString()
cookie.portlist = portList
cookieManager.cookieStore.add(redirectSourceUrl.toUri(), cookie)
client = client.newBuilder()
.cookieJar(JavaNetCookieJar(cookieManager))
.build()
get(redirectSourceUrl)
val request = redirectSource.takeRequest()
assertThat(request.headers["Cookie"]).isEqualTo("c=cookie")
for (header in redirectTarget.takeRequest().headers.names()) {
if (header.startsWith("Cookie")) {
fail<Any>(header)
}
}
}
@Test
fun testCookiesSentIgnoresCase() {
client = client.newBuilder()
.cookieJar(JavaNetCookieJar(object : CookieManager() {
override fun get(uri: URI, requestHeaders: Map<String, List<String>>) = mapOf(
"COOKIE" to listOf("Bar=bar"),
"cooKIE2" to listOf("Baz=baz"),
)
}))
.build()
server.enqueue(MockResponse())
get(server.url("/"))
val request = server.takeRequest()
assertThat(request.headers["Cookie"]).isEqualTo("Bar=bar; Baz=baz")
assertThat(request.headers["Cookie2"]).isNull()
assertThat(request.headers["Quux"]).isNull()
}
@Test
fun acceptOriginalServerMatchesSubdomain() {
val cookieManager = CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER)
val cookieJar = JavaNetCookieJar(cookieManager)
val url = "https://www.squareup.com/".toHttpUrl()
cookieJar.saveFromResponse(url, listOf(parse(url, "a=android; Domain=squareup.com")!!))
val actualCookies = cookieJar.loadForRequest(url)
assertThat(actualCookies.size).isEqualTo(1)
assertThat(actualCookies[0].name).isEqualTo("a")
assertThat(actualCookies[0].value).isEqualTo("android")
}
@Test
fun acceptOriginalServerMatchesRfc2965Dot() {
val cookieManager = CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER)
val cookieJar = JavaNetCookieJar(cookieManager)
val url = "https://www.squareup.com/".toHttpUrl()
cookieJar.saveFromResponse(url, listOf(parse(url, "a=android; Domain=.squareup.com")!!))
val actualCookies = cookieJar.loadForRequest(url)
assertThat(actualCookies.size).isEqualTo(1)
assertThat(actualCookies[0].name).isEqualTo("a")
assertThat(actualCookies[0].value).isEqualTo("android")
}
@Test
fun acceptOriginalServerMatchesExactly() {
val cookieManager = CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER)
val cookieJar = JavaNetCookieJar(cookieManager)
val url = "https://squareup.com/".toHttpUrl()
cookieJar.saveFromResponse(url, listOf(parse(url, "a=android; Domain=squareup.com")!!))
val actualCookies = cookieJar.loadForRequest(url)
assertThat(actualCookies.size).isEqualTo(1)
assertThat(actualCookies[0].name).isEqualTo("a")
assertThat(actualCookies[0].value).isEqualTo("android")
}
@Test
fun acceptOriginalServerDoesNotMatchDifferentServer() {
val cookieManager = CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER)
val cookieJar = JavaNetCookieJar(cookieManager)
val url1 = "https://api.squareup.com/".toHttpUrl()
cookieJar.saveFromResponse(url1, listOf(parse(url1, "a=android; Domain=api.squareup.com")!!))
val url2 = "https://www.squareup.com/".toHttpUrl()
val actualCookies = cookieJar.loadForRequest(url2)
assertThat(actualCookies).isEmpty()
}
@Test
fun testQuoteStripping() {
client = client.newBuilder()
.cookieJar(JavaNetCookieJar(object : CookieManager() {
override fun get(uri: URI, requestHeaders: Map<String, List<String>>) = mapOf(
"COOKIE" to listOf("Bar=\""),
"cooKIE2" to listOf("Baz=\"baz\""),
)
}))
.build()
server.enqueue(MockResponse())
get(server.url("/"))
val request = server.takeRequest()
assertThat(request.headers["Cookie"]).isEqualTo("Bar=\"; Baz=baz")
assertThat(request.headers["Cookie2"]).isNull()
assertThat(request.headers["Quux"]).isNull()
}
private fun urlWithIpAddress(server: MockWebServer, path: String): HttpUrl {
return server.url(path)
.newBuilder()
.host(InetAddress.getByName(server.hostName).hostAddress)
.build()
}
private operator fun get(url: HttpUrl) {
val call = client.newCall(
Request.Builder()
.url(url)
.build()
)
val response = call.execute()
response.body.close()
}
}

View File

@ -19,7 +19,8 @@ class DispatcherTest {
val clientTestRule = OkHttpClientTestRule()
private val executor = RecordingExecutor(this)
val callback = RecordingCallback()
val webSocketListener = RecordingWebSocketListener()
val webSocketListener = object : WebSocketListener() {
}
val dispatcher = Dispatcher(executor)
val listener = RecordingEventListener()
var client = clientTestRule.newClientBuilder()

View File

@ -1,62 +0,0 @@
/*
* Copyright 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.
*/
package okhttp3;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
/**
* An SSLSocketFactory that delegates calls. Sockets created by the delegate are wrapped with ones
* that will not accept the {@link #TLS_FALLBACK_SCSV} cipher, thus bypassing server-side fallback
* checks on platforms that support it. Unfortunately this wrapping will disable any
* reflection-based calls to SSLSocket from Platform.
*/
public class FallbackTestClientSocketFactory extends DelegatingSSLSocketFactory {
/**
* The cipher suite used during TLS connection fallback to indicate a fallback. See
* https://tools.ietf.org/html/draft-ietf-tls-downgrade-scsv-00
*/
public static final String TLS_FALLBACK_SCSV = "TLS_FALLBACK_SCSV";
public FallbackTestClientSocketFactory(SSLSocketFactory delegate) {
super(delegate);
}
@Override protected SSLSocket configureSocket(SSLSocket sslSocket) throws IOException {
return new TlsFallbackScsvDisabledSSLSocket(sslSocket);
}
private static class TlsFallbackScsvDisabledSSLSocket extends DelegatingSSLSocket {
public TlsFallbackScsvDisabledSSLSocket(SSLSocket socket) {
super(socket);
}
@Override public void setEnabledCipherSuites(String[] suites) {
List<String> enabledCipherSuites = new ArrayList<>(suites.length);
for (String suite : suites) {
if (!suite.equals(TLS_FALLBACK_SCSV)) {
enabledCipherSuites.add(suite);
}
}
getDelegate().setEnabledCipherSuites(
enabledCipherSuites.toArray(new String[enabledCipherSuites.size()]));
}
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright 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.
*/
package okhttp3
import javax.net.ssl.SSLSocket
import javax.net.ssl.SSLSocketFactory
import okhttp3.FallbackTestClientSocketFactory.Companion.TLS_FALLBACK_SCSV
/**
* An SSLSocketFactory that delegates calls. Sockets created by the delegate are wrapped with ones
* that will not accept the [TLS_FALLBACK_SCSV] cipher, thus bypassing server-side fallback
* checks on platforms that support it. Unfortunately this wrapping will disable any
* reflection-based calls to SSLSocket from Platform.
*/
class FallbackTestClientSocketFactory(
delegate: SSLSocketFactory,
) : DelegatingSSLSocketFactory(delegate) {
override fun configureSocket(sslSocket: SSLSocket): SSLSocket =
TlsFallbackScsvDisabledSSLSocket(sslSocket)
private class TlsFallbackScsvDisabledSSLSocket(
socket: SSLSocket
) : DelegatingSSLSocket(socket) {
override fun setEnabledCipherSuites(suites: Array<String>) {
val enabledCipherSuites = mutableListOf<String>()
for (suite in suites) {
if (suite != TLS_FALLBACK_SCSV) {
enabledCipherSuites.add(suite)
}
}
delegate!!.enabledCipherSuites = enabledCipherSuites.toTypedArray<String>()
}
}
companion object {
/**
* The cipher suite used during TLS connection fallback to indicate a fallback. See
* https://tools.ietf.org/html/draft-ietf-tls-downgrade-scsv-00
*/
const val TLS_FALLBACK_SCSV = "TLS_FALLBACK_SCSV"
}
}

View File

@ -1,218 +0,0 @@
/*
* 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.
*/
package okhttp3;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import okio.Buffer;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
public final class FormBodyTest {
@Test public void urlEncoding() throws Exception {
FormBody body = new FormBody.Builder()
.add("a+=& b", "c+=& d")
.add("space, the", "final frontier")
.add("%25", "%25")
.build();
assertThat(body.size()).isEqualTo(3);
assertThat(body.encodedName(0)).isEqualTo("a%2B%3D%26+b");
assertThat(body.encodedName(1)).isEqualTo("space%2C+the");
assertThat(body.encodedName(2)).isEqualTo("%2525");
assertThat(body.name(0)).isEqualTo("a+=& b");
assertThat(body.name(1)).isEqualTo("space, the");
assertThat(body.name(2)).isEqualTo("%25");
assertThat(body.encodedValue(0)).isEqualTo("c%2B%3D%26+d");
assertThat(body.encodedValue(1)).isEqualTo("final+frontier");
assertThat(body.encodedValue(2)).isEqualTo("%2525");
assertThat(body.value(0)).isEqualTo("c+=& d");
assertThat(body.value(1)).isEqualTo("final frontier");
assertThat(body.value(2)).isEqualTo("%25");
assertThat(body.contentType().toString()).isEqualTo(
"application/x-www-form-urlencoded");
String expected = "a%2B%3D%26+b=c%2B%3D%26+d&space%2C+the=final+frontier&%2525=%2525";
assertThat(body.contentLength()).isEqualTo(expected.length());
Buffer out = new Buffer();
body.writeTo(out);
assertThat(out.readUtf8()).isEqualTo(expected);
}
@Test public void addEncoded() throws Exception {
FormBody body = new FormBody.Builder()
.addEncoded("a+=& b", "c+=& d")
.addEncoded("e+=& f", "g+=& h")
.addEncoded("%25", "%25")
.build();
String expected = "a+%3D%26+b=c+%3D%26+d&e+%3D%26+f=g+%3D%26+h&%25=%25";
Buffer out = new Buffer();
body.writeTo(out);
assertThat(out.readUtf8()).isEqualTo(expected);
}
@Test public void encodedPair() throws Exception {
FormBody body = new FormBody.Builder()
.add("sim", "ple")
.build();
String expected = "sim=ple";
assertThat(body.contentLength()).isEqualTo(expected.length());
Buffer buffer = new Buffer();
body.writeTo(buffer);
assertThat(buffer.readUtf8()).isEqualTo(expected);
}
@Test public void encodeMultiplePairs() throws Exception {
FormBody body = new FormBody.Builder()
.add("sim", "ple")
.add("hey", "there")
.add("help", "me")
.build();
String expected = "sim=ple&hey=there&help=me";
assertThat(body.contentLength()).isEqualTo(expected.length());
Buffer buffer = new Buffer();
body.writeTo(buffer);
assertThat(buffer.readUtf8()).isEqualTo(expected);
}
@Test public void buildEmptyForm() throws Exception {
FormBody body = new FormBody.Builder().build();
String expected = "";
assertThat(body.contentLength()).isEqualTo(expected.length());
Buffer buffer = new Buffer();
body.writeTo(buffer);
assertThat(buffer.readUtf8()).isEqualTo(expected);
}
@Test public void characterEncoding() throws Exception {
// Browsers convert '\u0000' to '%EF%BF%BD'.
assertThat(formEncode(0)).isEqualTo("%00");
assertThat(formEncode(1)).isEqualTo("%01");
assertThat(formEncode(2)).isEqualTo("%02");
assertThat(formEncode(3)).isEqualTo("%03");
assertThat(formEncode(4)).isEqualTo("%04");
assertThat(formEncode(5)).isEqualTo("%05");
assertThat(formEncode(6)).isEqualTo("%06");
assertThat(formEncode(7)).isEqualTo("%07");
assertThat(formEncode(8)).isEqualTo("%08");
assertThat(formEncode(9)).isEqualTo("%09");
// Browsers convert '\n' to '\r\n'
assertThat(formEncode(10)).isEqualTo("%0A");
assertThat(formEncode(11)).isEqualTo("%0B");
assertThat(formEncode(12)).isEqualTo("%0C");
// Browsers convert '\r' to '\r\n'
assertThat(formEncode(13)).isEqualTo("%0D");
assertThat(formEncode(14)).isEqualTo("%0E");
assertThat(formEncode(15)).isEqualTo("%0F");
assertThat(formEncode(16)).isEqualTo("%10");
assertThat(formEncode(17)).isEqualTo("%11");
assertThat(formEncode(18)).isEqualTo("%12");
assertThat(formEncode(19)).isEqualTo("%13");
assertThat(formEncode(20)).isEqualTo("%14");
assertThat(formEncode(21)).isEqualTo("%15");
assertThat(formEncode(22)).isEqualTo("%16");
assertThat(formEncode(23)).isEqualTo("%17");
assertThat(formEncode(24)).isEqualTo("%18");
assertThat(formEncode(25)).isEqualTo("%19");
assertThat(formEncode(26)).isEqualTo("%1A");
assertThat(formEncode(27)).isEqualTo("%1B");
assertThat(formEncode(28)).isEqualTo("%1C");
assertThat(formEncode(29)).isEqualTo("%1D");
assertThat(formEncode(30)).isEqualTo("%1E");
assertThat(formEncode(31)).isEqualTo("%1F");
// Browsers use '+' for space.
assertThat(formEncode(32)).isEqualTo("+");
assertThat(formEncode(33)).isEqualTo("%21");
assertThat(formEncode(34)).isEqualTo("%22");
assertThat(formEncode(35)).isEqualTo("%23");
assertThat(formEncode(36)).isEqualTo("%24");
assertThat(formEncode(37)).isEqualTo("%25");
assertThat(formEncode(38)).isEqualTo("%26");
assertThat(formEncode(39)).isEqualTo("%27");
assertThat(formEncode(40)).isEqualTo("%28");
assertThat(formEncode(41)).isEqualTo("%29");
assertThat(formEncode(42)).isEqualTo("*");
assertThat(formEncode(43)).isEqualTo("%2B");
assertThat(formEncode(44)).isEqualTo("%2C");
assertThat(formEncode(45)).isEqualTo("-");
assertThat(formEncode(46)).isEqualTo(".");
assertThat(formEncode(47)).isEqualTo("%2F");
assertThat(formEncode(48)).isEqualTo("0");
assertThat(formEncode(57)).isEqualTo("9");
assertThat(formEncode(58)).isEqualTo("%3A");
assertThat(formEncode(59)).isEqualTo("%3B");
assertThat(formEncode(60)).isEqualTo("%3C");
assertThat(formEncode(61)).isEqualTo("%3D");
assertThat(formEncode(62)).isEqualTo("%3E");
assertThat(formEncode(63)).isEqualTo("%3F");
assertThat(formEncode(64)).isEqualTo("%40");
assertThat(formEncode(65)).isEqualTo("A");
assertThat(formEncode(90)).isEqualTo("Z");
assertThat(formEncode(91)).isEqualTo("%5B");
assertThat(formEncode(92)).isEqualTo("%5C");
assertThat(formEncode(93)).isEqualTo("%5D");
assertThat(formEncode(94)).isEqualTo("%5E");
assertThat(formEncode(95)).isEqualTo("_");
assertThat(formEncode(96)).isEqualTo("%60");
assertThat(formEncode(97)).isEqualTo("a");
assertThat(formEncode(122)).isEqualTo("z");
assertThat(formEncode(123)).isEqualTo("%7B");
assertThat(formEncode(124)).isEqualTo("%7C");
assertThat(formEncode(125)).isEqualTo("%7D");
assertThat(formEncode(126)).isEqualTo("%7E");
assertThat(formEncode(127)).isEqualTo("%7F");
assertThat(formEncode(128)).isEqualTo("%C2%80");
assertThat(formEncode(255)).isEqualTo("%C3%BF");
}
private String formEncode(int codePoint) throws IOException {
// Wrap the codepoint with regular printable characters to prevent trimming.
FormBody body = new FormBody.Builder()
.add("a", new String(new int[] {'b', codePoint, 'c'}, 0, 3))
.build();
Buffer buffer = new Buffer();
body.writeTo(buffer);
buffer.skip(3); // Skip "a=b" prefix.
return buffer.readUtf8(buffer.size() - 1); // Skip the "c" suffix.
}
@Test public void manualCharset() throws Exception {
FormBody body = new FormBody.Builder(StandardCharsets.ISO_8859_1)
.add("name", "Nicolás")
.build();
String expected = "name=Nicol%E1s";
assertThat(body.contentLength()).isEqualTo(expected.length());
Buffer out = new Buffer();
body.writeTo(out);
assertThat(out.readUtf8()).isEqualTo(expected);
}
}

View File

@ -0,0 +1,209 @@
/*
* 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.
*/
package okhttp3
import java.io.IOException
import java.nio.charset.StandardCharsets
import okio.Buffer
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
class FormBodyTest {
@Test
fun urlEncoding() {
val body = FormBody.Builder()
.add("a+=& b", "c+=& d")
.add("space, the", "final frontier")
.add("%25", "%25")
.build()
assertThat(body.size).isEqualTo(3)
assertThat(body.encodedName(0)).isEqualTo("a%2B%3D%26+b")
assertThat(body.encodedName(1)).isEqualTo("space%2C+the")
assertThat(body.encodedName(2)).isEqualTo("%2525")
assertThat(body.name(0)).isEqualTo("a+=& b")
assertThat(body.name(1)).isEqualTo("space, the")
assertThat(body.name(2)).isEqualTo("%25")
assertThat(body.encodedValue(0)).isEqualTo("c%2B%3D%26+d")
assertThat(body.encodedValue(1)).isEqualTo("final+frontier")
assertThat(body.encodedValue(2)).isEqualTo("%2525")
assertThat(body.value(0)).isEqualTo("c+=& d")
assertThat(body.value(1)).isEqualTo("final frontier")
assertThat(body.value(2)).isEqualTo("%25")
assertThat(body.contentType().toString()).isEqualTo(
"application/x-www-form-urlencoded"
)
val expected = "a%2B%3D%26+b=c%2B%3D%26+d&space%2C+the=final+frontier&%2525=%2525"
assertThat(body.contentLength()).isEqualTo(expected.length.toLong())
val out = Buffer()
body.writeTo(out)
assertThat(out.readUtf8()).isEqualTo(expected)
}
@Test
fun addEncoded() {
val body = FormBody.Builder()
.addEncoded("a+=& b", "c+=& d")
.addEncoded("e+=& f", "g+=& h")
.addEncoded("%25", "%25")
.build()
val expected = "a+%3D%26+b=c+%3D%26+d&e+%3D%26+f=g+%3D%26+h&%25=%25"
val out = Buffer()
body.writeTo(out)
assertThat(out.readUtf8()).isEqualTo(expected)
}
@Test
fun encodedPair() {
val body = FormBody.Builder()
.add("sim", "ple")
.build()
val expected = "sim=ple"
assertThat(body.contentLength()).isEqualTo(expected.length.toLong())
val buffer = Buffer()
body.writeTo(buffer)
assertThat(buffer.readUtf8()).isEqualTo(expected)
}
@Test
fun encodeMultiplePairs() {
val body = FormBody.Builder()
.add("sim", "ple")
.add("hey", "there")
.add("help", "me")
.build()
val expected = "sim=ple&hey=there&help=me"
assertThat(body.contentLength()).isEqualTo(expected.length.toLong())
val buffer = Buffer()
body.writeTo(buffer)
assertThat(buffer.readUtf8()).isEqualTo(expected)
}
@Test
fun buildEmptyForm() {
val body = FormBody.Builder().build()
val expected = ""
assertThat(body.contentLength()).isEqualTo(expected.length.toLong())
val buffer = Buffer()
body.writeTo(buffer)
assertThat(buffer.readUtf8()).isEqualTo(expected)
}
@Test
fun characterEncoding() {
// Browsers convert '\u0000' to '%EF%BF%BD'.
assertThat(formEncode(0)).isEqualTo("%00")
assertThat(formEncode(1)).isEqualTo("%01")
assertThat(formEncode(2)).isEqualTo("%02")
assertThat(formEncode(3)).isEqualTo("%03")
assertThat(formEncode(4)).isEqualTo("%04")
assertThat(formEncode(5)).isEqualTo("%05")
assertThat(formEncode(6)).isEqualTo("%06")
assertThat(formEncode(7)).isEqualTo("%07")
assertThat(formEncode(8)).isEqualTo("%08")
assertThat(formEncode(9)).isEqualTo("%09")
// Browsers convert '\n' to '\r\n'
assertThat(formEncode(10)).isEqualTo("%0A")
assertThat(formEncode(11)).isEqualTo("%0B")
assertThat(formEncode(12)).isEqualTo("%0C")
// Browsers convert '\r' to '\r\n'
assertThat(formEncode(13)).isEqualTo("%0D")
assertThat(formEncode(14)).isEqualTo("%0E")
assertThat(formEncode(15)).isEqualTo("%0F")
assertThat(formEncode(16)).isEqualTo("%10")
assertThat(formEncode(17)).isEqualTo("%11")
assertThat(formEncode(18)).isEqualTo("%12")
assertThat(formEncode(19)).isEqualTo("%13")
assertThat(formEncode(20)).isEqualTo("%14")
assertThat(formEncode(21)).isEqualTo("%15")
assertThat(formEncode(22)).isEqualTo("%16")
assertThat(formEncode(23)).isEqualTo("%17")
assertThat(formEncode(24)).isEqualTo("%18")
assertThat(formEncode(25)).isEqualTo("%19")
assertThat(formEncode(26)).isEqualTo("%1A")
assertThat(formEncode(27)).isEqualTo("%1B")
assertThat(formEncode(28)).isEqualTo("%1C")
assertThat(formEncode(29)).isEqualTo("%1D")
assertThat(formEncode(30)).isEqualTo("%1E")
assertThat(formEncode(31)).isEqualTo("%1F")
// Browsers use '+' for space.
assertThat(formEncode(32)).isEqualTo("+")
assertThat(formEncode(33)).isEqualTo("%21")
assertThat(formEncode(34)).isEqualTo("%22")
assertThat(formEncode(35)).isEqualTo("%23")
assertThat(formEncode(36)).isEqualTo("%24")
assertThat(formEncode(37)).isEqualTo("%25")
assertThat(formEncode(38)).isEqualTo("%26")
assertThat(formEncode(39)).isEqualTo("%27")
assertThat(formEncode(40)).isEqualTo("%28")
assertThat(formEncode(41)).isEqualTo("%29")
assertThat(formEncode(42)).isEqualTo("*")
assertThat(formEncode(43)).isEqualTo("%2B")
assertThat(formEncode(44)).isEqualTo("%2C")
assertThat(formEncode(45)).isEqualTo("-")
assertThat(formEncode(46)).isEqualTo(".")
assertThat(formEncode(47)).isEqualTo("%2F")
assertThat(formEncode(48)).isEqualTo("0")
assertThat(formEncode(57)).isEqualTo("9")
assertThat(formEncode(58)).isEqualTo("%3A")
assertThat(formEncode(59)).isEqualTo("%3B")
assertThat(formEncode(60)).isEqualTo("%3C")
assertThat(formEncode(61)).isEqualTo("%3D")
assertThat(formEncode(62)).isEqualTo("%3E")
assertThat(formEncode(63)).isEqualTo("%3F")
assertThat(formEncode(64)).isEqualTo("%40")
assertThat(formEncode(65)).isEqualTo("A")
assertThat(formEncode(90)).isEqualTo("Z")
assertThat(formEncode(91)).isEqualTo("%5B")
assertThat(formEncode(92)).isEqualTo("%5C")
assertThat(formEncode(93)).isEqualTo("%5D")
assertThat(formEncode(94)).isEqualTo("%5E")
assertThat(formEncode(95)).isEqualTo("_")
assertThat(formEncode(96)).isEqualTo("%60")
assertThat(formEncode(97)).isEqualTo("a")
assertThat(formEncode(122)).isEqualTo("z")
assertThat(formEncode(123)).isEqualTo("%7B")
assertThat(formEncode(124)).isEqualTo("%7C")
assertThat(formEncode(125)).isEqualTo("%7D")
assertThat(formEncode(126)).isEqualTo("%7E")
assertThat(formEncode(127)).isEqualTo("%7F")
assertThat(formEncode(128)).isEqualTo("%C2%80")
assertThat(formEncode(255)).isEqualTo("%C3%BF")
}
@Throws(IOException::class)
private fun formEncode(codePoint: Int): String {
// Wrap the codepoint with regular printable characters to prevent trimming.
val body = FormBody.Builder()
.add("a", String(intArrayOf('b'.code, codePoint, 'c'.code), 0, 3))
.build()
val buffer = Buffer()
body.writeTo(buffer)
buffer.skip(3) // Skip "a=b" prefix.
return buffer.readUtf8(buffer.size - 1) // Skip the "c" suffix.
}
@Test
fun manualCharset() {
val body = FormBody.Builder(StandardCharsets.ISO_8859_1)
.add("name", "Nicolás")
.build()
val expected = "name=Nicol%E1s"
assertThat(body.contentLength()).isEqualTo(expected.length.toLong())
val out = Buffer()
body.writeTo(out)
assertThat(out.readUtf8()).isEqualTo(expected)
}
}

View File

@ -1,52 +0,0 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3;
import java.io.IOException;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class ProtocolTest {
@Test
public void testGetKnown() throws IOException {
assertThat(Protocol.get("http/1.0")).isEqualTo(Protocol.HTTP_1_0);
assertThat(Protocol.get("http/1.1")).isEqualTo(Protocol.HTTP_1_1);
assertThat(Protocol.get("spdy/3.1")).isEqualTo(Protocol.SPDY_3);
assertThat(Protocol.get("h2")).isEqualTo(Protocol.HTTP_2);
assertThat(Protocol.get("h2_prior_knowledge")).isEqualTo(Protocol.H2_PRIOR_KNOWLEDGE);
assertThat(Protocol.get("quic")).isEqualTo(Protocol.QUIC);
assertThat(Protocol.get("h3")).isEqualTo(Protocol.HTTP_3);
assertThat(Protocol.get("h3-29")).isEqualTo(Protocol.HTTP_3);
}
@Test
public void testGetUnknown() {
assertThrows(IOException.class, () -> Protocol.get("tcp"));
}
@Test
public void testToString() {
assertThat(Protocol.HTTP_1_0.toString()).isEqualTo("http/1.0");
assertThat(Protocol.HTTP_1_1.toString()).isEqualTo("http/1.1");
assertThat(Protocol.SPDY_3.toString()).isEqualTo("spdy/3.1");
assertThat(Protocol.HTTP_2.toString()).isEqualTo("h2");
assertThat(Protocol.H2_PRIOR_KNOWLEDGE.toString()).isEqualTo("h2_prior_knowledge");
assertThat(Protocol.QUIC.toString()).isEqualTo("quic");
assertThat(Protocol.HTTP_3.toString()).isEqualTo("h3");
}
}

View File

@ -0,0 +1,53 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3
import java.io.IOException
import okhttp3.Protocol.Companion.get
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Test
class ProtocolTest {
@Test
fun testGetKnown() {
assertThat(get("http/1.0")).isEqualTo(Protocol.HTTP_1_0)
assertThat(get("http/1.1")).isEqualTo(Protocol.HTTP_1_1)
assertThat(get("spdy/3.1")).isEqualTo(Protocol.SPDY_3)
assertThat(get("h2")).isEqualTo(Protocol.HTTP_2)
assertThat(get("h2_prior_knowledge")).isEqualTo(Protocol.H2_PRIOR_KNOWLEDGE)
assertThat(get("quic")).isEqualTo(Protocol.QUIC)
assertThat(get("h3")).isEqualTo(Protocol.HTTP_3)
assertThat(get("h3-29")).isEqualTo(Protocol.HTTP_3)
}
@Test
fun testGetUnknown() {
assertThrows(IOException::class.java) { get("tcp") }
}
@Test
fun testToString() {
assertThat(Protocol.HTTP_1_0.toString()).isEqualTo("http/1.0")
assertThat(Protocol.HTTP_1_1.toString()).isEqualTo("http/1.1")
assertThat(Protocol.SPDY_3.toString()).isEqualTo("spdy/3.1")
assertThat(Protocol.HTTP_2.toString()).isEqualTo("h2")
assertThat(Protocol.H2_PRIOR_KNOWLEDGE.toString())
.isEqualTo("h2_prior_knowledge")
assertThat(Protocol.QUIC.toString()).isEqualTo("quic")
assertThat(Protocol.HTTP_3.toString()).isEqualTo("h3")
}
}

View File

@ -1,45 +0,0 @@
/*
* Copyright (C) 2019 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3;
import okhttp3.internal.http.HttpHeaders;
import okhttp3.internal.http.HttpMethod;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SuppressWarnings("ALL") public class PublicInternalApiTest {
@Test public void permitsRequestBody() {
assertTrue(HttpMethod.permitsRequestBody("POST"));
assertFalse(HttpMethod.permitsRequestBody("GET"));
}
@Test public void requiresRequestBody() {
assertTrue(HttpMethod.requiresRequestBody("PUT"));
assertFalse(HttpMethod.requiresRequestBody("GET"));
}
@Test public void hasBody() {
Request request = new Request.Builder().url("http://example.com").build();
Response response = new Response.Builder().code(200)
.message("OK")
.request(request)
.protocol(Protocol.HTTP_2)
.build();
assertTrue(HttpHeaders.hasBody(response));
}
}

View File

@ -0,0 +1,49 @@
/*
* Copyright (C) 2019 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3
import okhttp3.internal.http.HttpMethod.permitsRequestBody
import okhttp3.internal.http.HttpMethod.requiresRequestBody
import okhttp3.internal.http.hasBody
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
@Suppress("DEPRECATION_ERROR")
class PublicInternalApiTest {
@Test
fun permitsRequestBody() {
assertTrue(permitsRequestBody("POST"))
assertFalse(permitsRequestBody("GET"))
}
@Test
fun requiresRequestBody() {
assertTrue(requiresRequestBody("PUT"))
assertFalse(requiresRequestBody("GET"))
}
@Test
fun hasBody() {
val request = Request.Builder().url("http://example.com").build()
val response = Response.Builder().code(200)
.message("OK")
.request(request)
.protocol(Protocol.HTTP_2)
.build()
assertTrue(hasBody(response))
}
}

View File

@ -1,194 +0,0 @@
/*
* 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.
*/
package okhttp3;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import javax.annotation.Nullable;
import static org.assertj.core.api.Assertions.assertThat;
/**
* A received response or failure recorded by the response recorder.
*/
public final class RecordedResponse {
public final Request request;
public final @Nullable Response response;
public final @Nullable WebSocket webSocket;
public final @Nullable String body;
public final @Nullable IOException failure;
public RecordedResponse(Request request, @Nullable Response response,
@Nullable WebSocket webSocket, @Nullable String body, @Nullable IOException failure) {
this.request = request;
this.response = response;
this.webSocket = webSocket;
this.body = body;
this.failure = failure;
}
public RecordedResponse assertRequestUrl(HttpUrl url) {
assertThat(request.url()).isEqualTo(url);
return this;
}
public RecordedResponse assertRequestMethod(String method) {
assertThat(request.method()).isEqualTo(method);
return this;
}
public RecordedResponse assertRequestHeader(String name, String... values) {
assertThat(request.headers(name)).containsExactly(values);
return this;
}
public RecordedResponse assertCode(int expectedCode) {
assertThat(response == null ? null : response.code()).isEqualTo(expectedCode);
return this;
}
public RecordedResponse assertSuccessful() {
assertThat(failure).isNull();
assertThat(response.isSuccessful()).isTrue();
return this;
}
public RecordedResponse assertNotSuccessful() {
assertThat(response.isSuccessful()).isFalse();
return this;
}
public RecordedResponse assertHeader(String name, String... values) {
assertThat(response.headers(name)).containsExactly(values);
return this;
}
public RecordedResponse assertHeaders(Headers headers) {
assertThat(response.headers()).isEqualTo(headers);
return this;
}
public RecordedResponse assertBody(String expectedBody) {
assertThat(body).isEqualTo(expectedBody);
return this;
}
public RecordedResponse assertHandshake() {
Handshake handshake = response.handshake();
assertThat(handshake.tlsVersion()).isNotNull();
assertThat(handshake.cipherSuite()).isNotNull();
assertThat(handshake.peerPrincipal()).isNotNull();
assertThat(handshake.peerCertificates().size()).isEqualTo(1);
assertThat(handshake.localPrincipal()).isNull();
assertThat(handshake.localCertificates().size()).isEqualTo(0);
return this;
}
/**
* Asserts that the current response was redirected and returns the prior response.
*/
public RecordedResponse priorResponse() {
Response priorResponse = response.priorResponse();
assertThat(priorResponse).isNotNull();
return new RecordedResponse(priorResponse.request(), priorResponse, null, null, null);
}
/**
* Asserts that the current response used the network and returns the network response.
*/
public RecordedResponse networkResponse() {
Response networkResponse = response.networkResponse();
assertThat(networkResponse).isNotNull();
return new RecordedResponse(networkResponse.request(), networkResponse, null, null, null);
}
/** Asserts that the current response didn't use the network. */
public RecordedResponse assertNoNetworkResponse() {
assertThat(response.networkResponse()).isNull();
return this;
}
/** Asserts that the current response didn't use the cache. */
public RecordedResponse assertNoCacheResponse() {
assertThat(response.cacheResponse()).isNull();
return this;
}
/**
* Asserts that the current response used the cache and returns the cache response.
*/
public RecordedResponse cacheResponse() {
Response cacheResponse = response.cacheResponse();
assertThat(cacheResponse).isNotNull();
return new RecordedResponse(cacheResponse.request(), cacheResponse, null, null, null);
}
public RecordedResponse assertFailure(Class<?>... allowedExceptionTypes) {
boolean found = false;
for (Class<?> expectedClass : allowedExceptionTypes) {
if (expectedClass.isInstance(failure)) {
found = true;
break;
}
}
assertThat(found)
.overridingErrorMessage("Expected exception type among "
+ Arrays.toString(allowedExceptionTypes) + ", got " + failure)
.isTrue();
return this;
}
public RecordedResponse assertFailure(String... messages) {
assertThat(failure).overridingErrorMessage("No failure found").isNotNull();
assertThat(messages).contains(failure.getMessage());
return this;
}
public RecordedResponse assertFailureMatches(String... patterns) {
assertThat(failure).isNotNull();
for (String pattern : patterns) {
if (failure.getMessage().matches(pattern)) return this;
}
throw new AssertionError(failure.getMessage());
}
public RecordedResponse assertSentRequestAtMillis(long minimum, long maximum) {
assertDateInRange(minimum, response.sentRequestAtMillis(), maximum);
return this;
}
public RecordedResponse assertReceivedResponseAtMillis(long minimum, long maximum) {
assertDateInRange(minimum, response.receivedResponseAtMillis(), maximum);
return this;
}
private void assertDateInRange(long minimum, long actual, long maximum) {
assertThat(actual)
.overridingErrorMessage("%s <= %s <= %s", format(minimum), format(actual), format(maximum))
.isBetween(minimum, maximum);
}
private String format(long time) {
return new SimpleDateFormat("HH:mm:ss.SSS").format(new Date(time));
}
public String getBody() {
return body;
}
}

View File

@ -0,0 +1,157 @@
/*
* 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.
*/
package okhttp3
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Date
import org.assertj.core.api.Assertions.assertThat
/**
* A received response or failure recorded by the response recorder.
*/
class RecordedResponse(
@JvmField val request: Request,
val response: Response?,
val webSocket: WebSocket?,
val body: String?,
val failure: IOException?
) {
fun assertRequestUrl(url: HttpUrl) = apply {
assertThat(request.url).isEqualTo(url)
}
fun assertRequestMethod(method: String) = apply {
assertThat(request.method).isEqualTo(method)
}
fun assertRequestHeader(name: String, vararg values: String) = apply {
assertThat(request.headers(name)).containsExactly(*values)
}
fun assertCode(expectedCode: Int) = apply {
assertThat(response!!.code).isEqualTo(expectedCode)
}
fun assertSuccessful() = apply {
assertThat(failure).isNull()
assertThat(response!!.isSuccessful).isTrue()
}
fun assertNotSuccessful() = apply {
assertThat(response!!.isSuccessful).isFalse()
}
fun assertHeader(name: String, vararg values: String?) = apply {
assertThat(response!!.headers(name)).containsExactly(*values)
}
fun assertHeaders(headers: Headers) = apply {
assertThat(response!!.headers).isEqualTo(headers)
}
fun assertBody(expectedBody: String) = apply {
assertThat(body).isEqualTo(expectedBody)
}
fun assertHandshake() = apply {
val handshake = response!!.handshake!!
assertThat(handshake.tlsVersion).isNotNull()
assertThat(handshake.cipherSuite).isNotNull()
assertThat(handshake.peerPrincipal).isNotNull()
assertThat(handshake.peerCertificates.size).isEqualTo(1)
assertThat(handshake.localPrincipal).isNull()
assertThat(handshake.localCertificates.size).isEqualTo(0)
}
/**
* Asserts that the current response was redirected and returns the prior response.
*/
fun priorResponse(): RecordedResponse {
val priorResponse = response!!.priorResponse!!
return RecordedResponse(priorResponse.request, priorResponse, null, null, null)
}
/**
* Asserts that the current response used the network and returns the network response.
*/
fun networkResponse(): RecordedResponse {
val networkResponse = response!!.networkResponse!!
return RecordedResponse(networkResponse.request, networkResponse, null, null, null)
}
/** Asserts that the current response didn't use the network. */
fun assertNoNetworkResponse() = apply {
assertThat(response!!.networkResponse).isNull()
}
/** Asserts that the current response didn't use the cache. */
fun assertNoCacheResponse() = apply {
assertThat(response!!.cacheResponse).isNull()
}
/**
* Asserts that the current response used the cache and returns the cache response.
*/
fun cacheResponse(): RecordedResponse {
val cacheResponse = response!!.cacheResponse!!
return RecordedResponse(cacheResponse.request, cacheResponse, null, null, null)
}
fun assertFailure(vararg allowedExceptionTypes: Class<*>) = apply {
var found = false
for (expectedClass in allowedExceptionTypes) {
if (expectedClass.isInstance(failure)) {
found = true
break
}
}
assertThat(found)
.overridingErrorMessage(
"Expected exception type among "
+ allowedExceptionTypes.contentToString() + ", got " + failure
)
.isTrue()
}
fun assertFailure(vararg messages: String) = apply {
assertThat(failure).overridingErrorMessage("No failure found").isNotNull()
assertThat(messages).contains(failure!!.message)
}
fun assertFailureMatches(vararg patterns: String) = apply {
val message = failure!!.message!!
assertThat(patterns.firstOrNull { pattern ->
message.matches(pattern.toRegex())
}).isNotNull()
}
fun assertSentRequestAtMillis(minimum: Long, maximum: Long) = apply {
assertDateInRange(minimum, response!!.sentRequestAtMillis, maximum)
}
fun assertReceivedResponseAtMillis(minimum: Long, maximum: Long) = apply {
assertDateInRange(minimum, response!!.receivedResponseAtMillis, maximum)
}
private fun assertDateInRange(minimum: Long, actual: Long, maximum: Long) {
assertThat(actual)
.overridingErrorMessage("${format(minimum)} <= ${format(actual)} <= ${format(maximum)}")
.isBetween(minimum, maximum)
}
private fun format(time: Long) = SimpleDateFormat("HH:mm:ss.SSS").format(Date(time))
}

View File

@ -1,65 +0,0 @@
/*
* 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.
*/
package okhttp3;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Records received HTTP responses so they can be later retrieved by tests.
*/
public class RecordingCallback implements Callback {
public static final long TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(10);
private final List<RecordedResponse> responses = new ArrayList<>();
@Override public synchronized void onFailure(Call call, IOException e) {
responses.add(new RecordedResponse(call.request(), null, null, null, e));
notifyAll();
}
@Override public synchronized void onResponse(Call call, Response response) throws IOException {
String body = response.body().string();
responses.add(new RecordedResponse(call.request(), response, null, body, null));
notifyAll();
}
/**
* Returns the recorded response triggered by {@code request}. Throws if the response isn't
* enqueued before the timeout.
*/
public synchronized RecordedResponse await(HttpUrl url) throws Exception {
long timeoutMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) + TIMEOUT_MILLIS;
while (true) {
for (Iterator<RecordedResponse> i = responses.iterator(); i.hasNext(); ) {
RecordedResponse recordedResponse = i.next();
if (recordedResponse.request.url().equals(url)) {
i.remove();
return recordedResponse;
}
}
long nowMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
if (nowMillis >= timeoutMillis) break;
wait(timeoutMillis - nowMillis);
}
throw new AssertionError("Timed out waiting for response to " + url);
}
}

View File

@ -0,0 +1,67 @@
/*
* 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.
*/
package okhttp3
import java.io.IOException
import java.util.concurrent.TimeUnit
/**
* Records received HTTP responses so they can be later retrieved by tests.
*/
class RecordingCallback : Callback {
private val responses = mutableListOf<RecordedResponse>()
@Synchronized
override fun onFailure(call: Call, e: IOException) {
responses.add(RecordedResponse(call.request(), null, null, null, e))
(this as Object).notifyAll()
}
@Synchronized
override fun onResponse(call: Call, response: Response) {
val body = response.body.string()
responses.add(RecordedResponse(call.request(), response, null, body, null))
(this as Object).notifyAll()
}
/**
* Returns the recorded response triggered by `request`. Throws if the response isn't
* enqueued before the timeout.
*/
@Synchronized
fun await(url: HttpUrl): RecordedResponse {
val timeoutMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) + TIMEOUT_MILLIS
while (true) {
val i = responses.iterator()
while (i.hasNext()) {
val recordedResponse = i.next()
if (recordedResponse.request.url.equals(url)) {
i.remove()
return recordedResponse
}
}
val nowMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime())
if (nowMillis >= timeoutMillis) break
(this as Object).wait(timeoutMillis - nowMillis)
}
throw AssertionError("Timed out waiting for response to $url")
}
companion object {
val TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(10)
}
}

View File

@ -1,45 +0,0 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3;
import javax.annotation.Nullable;
import okio.ByteString;
public final class RecordingWebSocketListener extends WebSocketListener {
@Override public void onOpen(WebSocket webSocket, Response response) {
// TODO
}
@Override public void onMessage(WebSocket webSocket, String text) {
// TODO
}
@Override public void onMessage(WebSocket webSocket, ByteString bytes) {
// TODO
}
@Override public void onClosing(WebSocket webSocket, int code, String reason) {
// TODO
}
@Override public void onClosed(WebSocket webSocket, int code, String reason) {
// TODO
}
@Override public void onFailure(WebSocket webSocket, Throwable t, @Nullable Response response) {
// TODO
}
}

View File

@ -1,114 +0,0 @@
/*
* 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.
*/
package okhttp3;
import java.io.IOException;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.SocketAddress;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import mockwebserver3.MockResponse;
import mockwebserver3.MockWebServer;
import okhttp3.testing.PlatformRule;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import static org.assertj.core.api.Assertions.assertThat;
public final class SocksProxyTest {
@RegisterExtension public final PlatformRule platform = new PlatformRule();
@RegisterExtension public final OkHttpClientTestRule clientTestRule = new OkHttpClientTestRule();
private MockWebServer server;
private final SocksProxy socksProxy = new SocksProxy();
@BeforeEach public void setUp(MockWebServer server) throws Exception {
this.server = server;
socksProxy.play();
}
@AfterEach public void tearDown() throws Exception {
socksProxy.shutdown();
}
@Test public void proxy() throws Exception {
server.enqueue(new MockResponse.Builder().body("abc").build());
server.enqueue(new MockResponse.Builder().body("def").build());
OkHttpClient client = clientTestRule.newClientBuilder()
.proxy(socksProxy.proxy())
.build();
Request request1 = new Request.Builder().url(server.url("/")).build();
Response response1 = client.newCall(request1).execute();
assertThat(response1.body().string()).isEqualTo("abc");
Request request2 = new Request.Builder().url(server.url("/")).build();
Response response2 = client.newCall(request2).execute();
assertThat(response2.body().string()).isEqualTo("def");
// The HTTP calls should share a single connection.
assertThat(socksProxy.connectionCount()).isEqualTo(1);
}
@Test public void proxySelector() throws Exception {
server.enqueue(new MockResponse.Builder().body("abc").build());
ProxySelector proxySelector = new ProxySelector() {
@Override public List<Proxy> select(URI uri) {
return Collections.singletonList(socksProxy.proxy());
}
@Override public void connectFailed(URI uri, SocketAddress socketAddress, IOException e) {
throw new AssertionError();
}
};
OkHttpClient client = clientTestRule.newClientBuilder()
.proxySelector(proxySelector)
.build();
Request request = new Request.Builder().url(server.url("/")).build();
Response response = client.newCall(request).execute();
assertThat(response.body().string()).isEqualTo("abc");
assertThat(socksProxy.connectionCount()).isEqualTo(1);
}
@Test public void checkRemoteDNSResolve() throws Exception {
// This testcase will fail if the target is resolved locally instead of through the proxy.
server.enqueue(new MockResponse.Builder().body("abc").build());
OkHttpClient client = clientTestRule.newClientBuilder()
.proxy(socksProxy.proxy())
.build();
HttpUrl url = server.url("/")
.newBuilder()
.host(SocksProxy.HOSTNAME_THAT_ONLY_THE_PROXY_KNOWS)
.build();
Request request = new Request.Builder().url(url).build();
Response response1 = client.newCall(request).execute();
assertThat(response1.body().string()).isEqualTo("abc");
assertThat(socksProxy.connectionCount()).isEqualTo(1);
}
}

View File

@ -0,0 +1,106 @@
/*
* 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.
*/
package okhttp3
import java.io.IOException
import java.net.ProxySelector
import java.net.SocketAddress
import java.net.URI
import mockwebserver3.MockResponse
import mockwebserver3.MockWebServer
import okhttp3.testing.PlatformRule
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
class SocksProxyTest {
@RegisterExtension
val platform = PlatformRule()
@RegisterExtension
val clientTestRule = OkHttpClientTestRule()
private lateinit var server: MockWebServer
private val socksProxy = SocksProxy()
@BeforeEach
fun setUp(server: MockWebServer) {
this.server = server
socksProxy.play()
}
@AfterEach
fun tearDown() {
socksProxy.shutdown()
}
@Test
fun proxy() {
server.enqueue(MockResponse.Builder().body("abc").build())
server.enqueue(MockResponse.Builder().body("def").build())
val client = clientTestRule.newClientBuilder()
.proxy(socksProxy.proxy())
.build()
val request1 = Request.Builder().url(server.url("/")).build()
val response1 = client.newCall(request1).execute()
assertThat(response1.body.string()).isEqualTo("abc")
val request2 = Request.Builder().url(server.url("/")).build()
val response2 = client.newCall(request2).execute()
assertThat(response2.body.string()).isEqualTo("def")
// The HTTP calls should share a single connection.
assertThat(socksProxy.connectionCount()).isEqualTo(1)
}
@Test
fun proxySelector() {
server.enqueue(MockResponse.Builder().body("abc").build())
val proxySelector: ProxySelector = object : ProxySelector() {
override fun select(uri: URI) = listOf(socksProxy.proxy())
override fun connectFailed(
uri: URI,
socketAddress: SocketAddress,
e: IOException,
) = error("unexpected call")
}
val client = clientTestRule.newClientBuilder()
.proxySelector(proxySelector)
.build()
val request = Request.Builder().url(server.url("/")).build()
val response = client.newCall(request).execute()
assertThat(response.body.string()).isEqualTo("abc")
assertThat(socksProxy.connectionCount()).isEqualTo(1)
}
@Test
fun checkRemoteDNSResolve() {
// This testcase will fail if the target is resolved locally instead of through the proxy.
server.enqueue(MockResponse.Builder().body("abc").build())
val client = clientTestRule.newClientBuilder()
.proxy(socksProxy.proxy())
.build()
val url = server.url("/")
.newBuilder()
.host(SocksProxy.HOSTNAME_THAT_ONLY_THE_PROXY_KNOWS)
.build()
val request = Request.Builder().url(url).build()
val response1 = client.newCall(request).execute()
assertThat(response1.body.string()).isEqualTo("abc")
assertThat(socksProxy.connectionCount()).isEqualTo(1)
}
}

View File

@ -1,101 +0,0 @@
/*
* 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.
*/
package okhttp3;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
/**
* A log handler that records which log messages were published so that a calling test can make
* assertions about them.
*/
public final class TestLogHandler implements TestRule, BeforeEachCallback, AfterEachCallback {
private final Logger logger;
private final BlockingQueue<String> logs = new LinkedBlockingQueue<>();
private final Handler handler = new Handler() {
@Override public void publish(LogRecord logRecord) {
logs.add(logRecord.getLevel() + ": " + logRecord.getMessage());
}
@Override public void flush() {
}
@Override public void close() throws SecurityException {
}
};
private Level previousLevel;
public TestLogHandler(Class<?> loggerName) {
logger = Logger.getLogger(loggerName.getName());
}
public TestLogHandler(Logger logger) {
this.logger = logger;
}
@Override public void beforeEach(ExtensionContext context) {
previousLevel = logger.getLevel();
logger.addHandler(handler);
logger.setLevel(Level.FINEST);
}
@Override public void afterEach(ExtensionContext context) {
logger.setLevel(previousLevel);
logger.removeHandler(handler);
}
@Override public Statement apply(Statement base, Description description) {
return new Statement() {
@Override public void evaluate() throws Throwable {
beforeEach(null);
try {
base.evaluate();
} finally {
afterEach(null);
}
}
};
}
public List<String> takeAll() {
List<String> list = new ArrayList<>();
logs.drainTo(list);
return list;
}
public String take() throws Exception {
String message = logs.poll(10, TimeUnit.SECONDS);
if (message == null) {
throw new AssertionError("Timed out waiting for log message.");
}
return message;
}
}

View File

@ -0,0 +1,93 @@
/*
* 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.
*/
package okhttp3
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.TimeUnit
import java.util.logging.Handler
import java.util.logging.Level
import java.util.logging.LogRecord
import java.util.logging.Logger
import org.junit.jupiter.api.extension.AfterEachCallback
import org.junit.jupiter.api.extension.BeforeEachCallback
import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
/**
* A log handler that records which log messages were published so that a calling test can make
* assertions about them.
*/
class TestLogHandler(
private val logger: Logger,
) : TestRule, BeforeEachCallback, AfterEachCallback {
constructor(loggerName: Class<*>) : this(Logger.getLogger(loggerName.getName()))
private val logs = LinkedBlockingQueue<String>()
private val handler = object : Handler() {
override fun publish(logRecord: LogRecord) {
logs += "${logRecord.level}: ${logRecord.message}"
}
override fun flush() {
}
override fun close() {
}
}
private var previousLevel: Level? = null
override fun beforeEach(context: ExtensionContext?) {
previousLevel = logger.level
logger.addHandler(handler)
logger.setLevel(Level.FINEST)
}
override fun afterEach(context: ExtensionContext?) {
logger.setLevel(previousLevel)
logger.removeHandler(handler)
}
override fun apply(
base: Statement,
description: Description,
): Statement {
return object : Statement() {
override fun evaluate() {
beforeEach(null)
try {
base.evaluate()
} finally {
afterEach(null)
}
}
}
}
fun takeAll(): List<String> {
val list = mutableListOf<String>()
logs.drainTo(list)
return list
}
fun take(): String {
return logs.poll(10, TimeUnit.SECONDS)
?: throw AssertionError("Timed out waiting for log message.")
}
}

View File

@ -1,106 +0,0 @@
package okhttp3;
import java.io.IOException;
import java.security.Security;
import java.util.List;
import okhttp3.internal.platform.Platform;
import org.conscrypt.Conscrypt;
import static java.util.Arrays.asList;
public class TestTls13Request {
// TLS 1.3
private static final CipherSuite[] TLS13_CIPHER_SUITES = new CipherSuite[] {
CipherSuite.TLS_AES_128_GCM_SHA256,
CipherSuite.TLS_AES_256_GCM_SHA384,
CipherSuite.TLS_CHACHA20_POLY1305_SHA256,
CipherSuite.TLS_AES_128_CCM_SHA256,
CipherSuite.TLS_AES_128_CCM_8_SHA256
};
/**
* A TLS 1.3 only Connection Spec. This will be eventually be exposed
* as part of MODERN_TLS or folded into the default OkHttp client once published and
* available in JDK11 or Conscrypt.
*/
private static final ConnectionSpec TLS_13 = new ConnectionSpec.Builder(true)
.cipherSuites(TLS13_CIPHER_SUITES)
.tlsVersions(TlsVersion.TLS_1_3)
.build();
private static final ConnectionSpec TLS_12 =
new ConnectionSpec.Builder(ConnectionSpec.RESTRICTED_TLS).tlsVersions(TlsVersion.TLS_1_2)
.build();
private TestTls13Request() {
}
public static void main(String[] args) {
//System.setProperty("javax.net.debug", "ssl:handshake:verbose");
Security.insertProviderAt(Conscrypt.newProviderBuilder().provideTrustManager().build(), 1);
System.out.println(
"Running tests using " + Platform.get() + " " + System.getProperty("java.vm.version"));
// https://github.com/tlswg/tls13-spec/wiki/Implementations
List<String> urls =
asList("https://enabled.tls13.com", "https://www.howsmyssl.com/a/check",
"https://tls13.cloudflare.com", "https://www.allizom.org/robots.txt",
"https://tls13.crypto.mozilla.org/", "https://tls.ctf.network/robots.txt",
"https://rustls.jbp.io/", "https://h2o.examp1e.net", "https://mew.org/",
"https://tls13.baishancloud.com/", "https://tls13.akamai.io/", "https://swifttls.org/",
"https://www.googleapis.com/robots.txt", "https://graph.facebook.com/robots.txt",
"https://api.twitter.com/robots.txt", "https://connect.squareup.com/robots.txt");
System.out.println("TLS1.3+TLS1.2");
testClient(urls, buildClient(ConnectionSpec.RESTRICTED_TLS));
System.out.println("\nTLS1.3 only");
testClient(urls, buildClient(TLS_13));
System.out.println("\nTLS1.3 then fallback");
testClient(urls, buildClient(TLS_13, TLS_12));
}
private static void testClient(List<String> urls, OkHttpClient client) {
try {
for (String url : urls) {
sendRequest(client, url);
}
} finally {
client.dispatcher().executorService().shutdownNow();
client.connectionPool().evictAll();
}
}
private static OkHttpClient buildClient(ConnectionSpec... specs) {
return new OkHttpClient.Builder().connectionSpecs(asList(specs)).build();
}
private static void sendRequest(OkHttpClient client, String url) {
System.out.printf("%-40s ", url);
System.out.flush();
System.out.println(Platform.get());
Request request = new Request.Builder().url(url).build();
try (Response response = client.newCall(request).execute()) {
Handshake handshake = response.handshake();
System.out.println(handshake.tlsVersion()
+ " "
+ handshake.cipherSuite()
+ " "
+ response.protocol()
+ " "
+ response.code()
+ " "
+ response.body().bytes().length
+ "b");
} catch (IOException ioe) {
System.out.println(ioe);
}
}
}

View File

@ -0,0 +1,101 @@
package okhttp3
import java.io.IOException
import java.security.Security
import okhttp3.internal.platform.Platform
import org.conscrypt.Conscrypt
// TLS 1.3
private val TLS13_CIPHER_SUITES = listOf(
CipherSuite.TLS_AES_128_GCM_SHA256,
CipherSuite.TLS_AES_256_GCM_SHA384,
CipherSuite.TLS_CHACHA20_POLY1305_SHA256,
CipherSuite.TLS_AES_128_CCM_SHA256,
CipherSuite.TLS_AES_128_CCM_8_SHA256
)
/**
* A TLS 1.3 only Connection Spec. This will be eventually be exposed
* as part of MODERN_TLS or folded into the default OkHttp client once published and
* available in JDK11 or Conscrypt.
*/
private val TLS_13 = ConnectionSpec.Builder(true)
.cipherSuites(*TLS13_CIPHER_SUITES.toTypedArray())
.tlsVersions(TlsVersion.TLS_1_3)
.build()
private val TLS_12 = ConnectionSpec.Builder(ConnectionSpec.RESTRICTED_TLS)
.tlsVersions(TlsVersion.TLS_1_2)
.build()
private fun testClient(urls: List<String>, client: OkHttpClient) {
try {
for (url in urls) {
sendRequest(client, url)
}
} finally {
client.dispatcher.executorService.shutdownNow()
client.connectionPool.evictAll()
}
}
private fun buildClient(vararg specs: ConnectionSpec): OkHttpClient {
return OkHttpClient.Builder()
.connectionSpecs(listOf(*specs))
.build()
}
private fun sendRequest(client: OkHttpClient, url: String) {
System.out.printf("%-40s ", url)
System.out.flush()
println(Platform.get())
val request = Request.Builder()
.url(url)
.build()
try {
client.newCall(request).execute().use { response ->
val handshake = response.handshake
println(
"${handshake!!.tlsVersion} ${handshake.cipherSuite} ${response.protocol} " +
"${response.code} ${response.body.bytes().size}b"
)
}
} catch (ioe: IOException) {
println(ioe)
}
}
fun main(vararg args: String) {
// System.setProperty("javax.net.debug", "ssl:handshake:verbose");
Security.insertProviderAt(Conscrypt.newProviderBuilder().provideTrustManager().build(), 1)
println("Running tests using ${Platform.get()} ${System.getProperty("java.vm.version")}")
// https://github.com/tlswg/tls13-spec/wiki/Implementations
val urls = listOf(
"https://enabled.tls13.com",
"https://www.howsmyssl.com/a/check",
"https://tls13.cloudflare.com",
"https://www.allizom.org/robots.txt",
"https://tls13.crypto.mozilla.org/",
"https://tls.ctf.network/robots.txt",
"https://rustls.jbp.io/",
"https://h2o.examp1e.net",
"https://mew.org/",
"https://tls13.baishancloud.com/",
"https://tls13.akamai.io/",
"https://swifttls.org/",
"https://www.googleapis.com/robots.txt",
"https://graph.facebook.com/robots.txt",
"https://api.twitter.com/robots.txt",
"https://connect.squareup.com/robots.txt",
)
println("TLS1.3+TLS1.2")
testClient(urls, buildClient(ConnectionSpec.RESTRICTED_TLS))
println("\nTLS1.3 only")
testClient(urls, buildClient(TLS_13))
println("\nTLS1.3 then fallback")
testClient(urls, buildClient(TLS_13, TLS_12))
}

View File

@ -13,22 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.internal;
package okhttp3.internal
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.List;
import okhttp3.Dns;
import static java.util.Arrays.asList;
import java.net.InetAddress
import okhttp3.Dns
/**
* A network that always resolves two IP addresses per host. Use this when testing route selection
* fallbacks to guarantee that a fallback address is available.
*/
public class DoubleInetAddressDns implements Dns {
@Override public List<InetAddress> lookup(String hostname) throws UnknownHostException {
List<InetAddress> addresses = Dns.SYSTEM.lookup(hostname);
return asList(addresses.get(0), addresses.get(0));
class DoubleInetAddressDns : Dns {
override fun lookup(hostname: String): List<InetAddress> {
val addresses = Dns.SYSTEM.lookup(hostname)
return listOf(addresses[0], addresses[0])
}
}

View File

@ -1,49 +0,0 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.internal;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
import java.util.ArrayList;
import java.util.List;
public final class RecordingAuthenticator extends Authenticator {
/** base64("username:password") */
public static final String BASE_64_CREDENTIALS = "dXNlcm5hbWU6cGFzc3dvcmQ=";
public final List<String> calls = new ArrayList<>();
public final PasswordAuthentication authentication;
public RecordingAuthenticator(PasswordAuthentication authentication) {
this.authentication = authentication;
}
public RecordingAuthenticator() {
this(new PasswordAuthentication("username", "password".toCharArray()));
}
@Override protected PasswordAuthentication getPasswordAuthentication() {
this.calls.add("host=" + getRequestingHost()
+ " port=" + getRequestingPort()
+ " site=" + getRequestingSite().getHostName()
+ " url=" + getRequestingURL()
+ " type=" + getRequestorType()
+ " prompt=" + getRequestingPrompt()
+ " protocol=" + getRequestingProtocol()
+ " scheme=" + getRequestingScheme());
return authentication;
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.internal
import java.net.Authenticator
import java.net.PasswordAuthentication
class RecordingAuthenticator(
private val authentication: PasswordAuthentication? = PasswordAuthentication(
"username",
"password".toCharArray()
)
) : Authenticator() {
val calls = mutableListOf<String>()
override fun getPasswordAuthentication(): PasswordAuthentication? {
calls.add("host=$requestingHost port=$requestingPort site=${requestingSite.hostName} " +
"url=$requestingURL type=$requestorType prompt=$requestingPrompt " +
"protocol=$requestingProtocol scheme=$requestingScheme"
)
return authentication
}
companion object {
/** base64("username:password") */
const val BASE_64_CREDENTIALS = "dXNlcm5hbWU6cGFzc3dvcmQ="
}
}

View File

@ -1,206 +0,0 @@
/*
* Copyright (C) 2016 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.internal;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.Socket;
import java.util.Deque;
import java.util.concurrent.LinkedBlockingDeque;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import okhttp3.DelegatingSSLSocket;
import okhttp3.DelegatingSSLSocketFactory;
import okio.Buffer;
import okio.ByteString;
/** Records all bytes written and read from a socket and makes them available for inspection. */
public final class SocketRecorder {
private final Deque<RecordedSocket> recordedSockets = new LinkedBlockingDeque<>();
/** Returns an SSLSocketFactory whose sockets will record all transmitted bytes. */
public SSLSocketFactory sslSocketFactory(SSLSocketFactory delegate) {
return new DelegatingSSLSocketFactory(delegate) {
@Override protected SSLSocket configureSocket(SSLSocket sslSocket) throws IOException {
RecordedSocket recordedSocket = new RecordedSocket();
recordedSockets.add(recordedSocket);
return new RecordingSSLSocket(sslSocket, recordedSocket);
}
};
}
public RecordedSocket takeSocket() {
return recordedSockets.remove();
}
/** A bidirectional transfer of unadulterated bytes over a socket. */
public static final class RecordedSocket {
private final Buffer bytesWritten = new Buffer();
private final Buffer bytesRead = new Buffer();
synchronized void byteWritten(int b) {
bytesWritten.writeByte(b);
}
synchronized void byteRead(int b) {
bytesRead.writeByte(b);
}
synchronized void bytesWritten(byte[] bytes, int offset, int length) {
bytesWritten.write(bytes, offset, length);
}
synchronized void bytesRead(byte[] bytes, int offset, int length) {
bytesRead.write(bytes, offset, length);
}
/** Returns all bytes that have been written to this socket. */
public synchronized ByteString bytesWritten() {
return bytesWritten.readByteString();
}
/** Returns all bytes that have been read from this socket. */
public synchronized ByteString bytesRead() {
return bytesRead.readByteString();
}
}
static final class RecordingInputStream extends InputStream {
private final Socket socket;
private final RecordedSocket recordedSocket;
RecordingInputStream(Socket socket, RecordedSocket recordedSocket) {
this.socket = socket;
this.recordedSocket = recordedSocket;
}
@Override public int read() throws IOException {
int b = socket.getInputStream().read();
if (b == -1) return -1;
recordedSocket.byteRead(b);
return b;
}
@Override public int read(byte[] b, int off, int len) throws IOException {
int read = socket.getInputStream().read(b, off, len);
if (read == -1) return -1;
recordedSocket.bytesRead(b, off, read);
return read;
}
@Override public void close() throws IOException {
socket.getInputStream().close();
}
}
static final class RecordingOutputStream extends OutputStream {
private final Socket socket;
private final RecordedSocket recordedSocket;
RecordingOutputStream(Socket socket, RecordedSocket recordedSocket) {
this.socket = socket;
this.recordedSocket = recordedSocket;
}
@Override public void write(int b) throws IOException {
socket.getOutputStream().write(b);
recordedSocket.byteWritten(b);
}
@Override public void write(byte[] b, int off, int len) throws IOException {
socket.getOutputStream().write(b, off, len);
recordedSocket.bytesWritten(b, off, len);
}
@Override public void close() throws IOException {
socket.getOutputStream().close();
}
@Override public void flush() throws IOException {
socket.getOutputStream().flush();
}
}
static final class RecordingSSLSocket extends DelegatingSSLSocket {
private final InputStream inputStream;
private final OutputStream outputStream;
RecordingSSLSocket(SSLSocket delegate, RecordedSocket recordedSocket) {
super(delegate);
inputStream = new RecordingInputStream(delegate, recordedSocket);
outputStream = new RecordingOutputStream(delegate, recordedSocket);
}
@Override public void startHandshake() throws IOException {
// Intercept the handshake to properly configure TLS extensions with Jetty ALPN. Jetty ALPN
// expects the real SSLSocket to be placed in the global map. Because we are wrapping the real
// SSLSocket, it confuses Jetty ALPN. This patches that up so things work as expected.
Class<?> alpn = null;
Class<?> provider = null;
try {
alpn = Class.forName("org.eclipse.jetty.alpn.ALPN");
provider = Class.forName("org.eclipse.jetty.alpn.ALPN$Provider");
} catch (ClassNotFoundException ignored) {
}
if (alpn == null || provider == null) {
// No Jetty, so nothing to worry about.
super.startHandshake();
return;
}
Object providerInstance = null;
Method putMethod = null;
try {
Method getMethod = alpn.getMethod("get", SSLSocket.class);
putMethod = alpn.getMethod("put", SSLSocket.class, provider);
providerInstance = getMethod.invoke(null, this);
if (providerInstance == null) {
// Jetty's on the classpath but TLS extensions weren't used.
super.startHandshake();
return;
}
// TLS extensions were used; replace with the real SSLSocket to make Jetty ALPN happy.
putMethod.invoke(null, getDelegate(), providerInstance);
super.startHandshake();
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
throw new AssertionError();
} finally {
// If we replaced the SSLSocket in the global map, we must put the original back for
// everything to work inside OkHttp.
if (providerInstance != null) {
try {
putMethod.invoke(null, this, providerInstance);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new AssertionError();
}
}
}
}
@Override public InputStream getInputStream() {
return inputStream;
}
@Override public OutputStream getOutputStream() {
return outputStream;
}
}
}

View File

@ -1,118 +0,0 @@
/*
* Copyright (C) 2012 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.internal.http;
import java.io.IOException;
import java.net.ProtocolException;
import okhttp3.Protocol;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail;
public final class StatusLineTest {
@Test public void parse() throws IOException {
String message = "Temporary Redirect";
int version = 1;
int code = 200;
StatusLine statusLine = StatusLine.Companion.parse(
"HTTP/1." + version + " " + code + " " + message);
assertThat(statusLine.message).isEqualTo(message);
assertThat(statusLine.protocol).isEqualTo(Protocol.HTTP_1_1);
assertThat(statusLine.code).isEqualTo(code);
}
@Test public void emptyMessage() throws IOException {
int version = 1;
int code = 503;
StatusLine statusLine = StatusLine.Companion.parse("HTTP/1." + version + " " + code + " ");
assertThat(statusLine.message).isEqualTo("");
assertThat(statusLine.protocol).isEqualTo(Protocol.HTTP_1_1);
assertThat(statusLine.code).isEqualTo(code);
}
/**
* This is not defined in the protocol but some servers won't add the leading empty space when the
* message is empty. http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1
*/
@Test public void emptyMessageAndNoLeadingSpace() throws IOException {
int version = 1;
int code = 503;
StatusLine statusLine = StatusLine.Companion.parse("HTTP/1." + version + " " + code);
assertThat(statusLine.message).isEqualTo("");
assertThat(statusLine.protocol).isEqualTo(Protocol.HTTP_1_1);
assertThat(statusLine.code).isEqualTo(code);
}
// https://github.com/square/okhttp/issues/386
@Test public void shoutcast() throws IOException {
StatusLine statusLine = StatusLine.Companion.parse("ICY 200 OK");
assertThat(statusLine.message).isEqualTo("OK");
assertThat(statusLine.protocol).isEqualTo(Protocol.HTTP_1_0);
assertThat(statusLine.code).isEqualTo(200);
}
@Test public void missingProtocol() throws IOException {
assertInvalid("");
assertInvalid(" ");
assertInvalid("200 OK");
assertInvalid(" 200 OK");
}
@Test public void protocolVersions() throws IOException {
assertInvalid("HTTP/2.0 200 OK");
assertInvalid("HTTP/2.1 200 OK");
assertInvalid("HTTP/-.1 200 OK");
assertInvalid("HTTP/1.- 200 OK");
assertInvalid("HTTP/0.1 200 OK");
assertInvalid("HTTP/101 200 OK");
assertInvalid("HTTP/1.1_200 OK");
}
@Test public void nonThreeDigitCode() throws IOException {
assertInvalid("HTTP/1.1 OK");
assertInvalid("HTTP/1.1 2 OK");
assertInvalid("HTTP/1.1 20 OK");
assertInvalid("HTTP/1.1 2000 OK");
assertInvalid("HTTP/1.1 two OK");
assertInvalid("HTTP/1.1 2");
assertInvalid("HTTP/1.1 2000");
assertInvalid("HTTP/1.1 two");
}
@Test public void truncated() throws IOException {
assertInvalid("");
assertInvalid("H");
assertInvalid("HTTP/1");
assertInvalid("HTTP/1.");
assertInvalid("HTTP/1.1");
assertInvalid("HTTP/1.1 ");
assertInvalid("HTTP/1.1 2");
assertInvalid("HTTP/1.1 20");
}
@Test public void wrongMessageDelimiter() throws IOException {
assertInvalid("HTTP/1.1 200_");
}
private void assertInvalid(String statusLine) throws IOException {
try {
StatusLine.Companion.parse(statusLine);
fail("");
} catch (ProtocolException expected) {
}
}
}

View File

@ -0,0 +1,125 @@
/*
* Copyright (C) 2012 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.internal.http
import java.net.ProtocolException
import okhttp3.Protocol
import okhttp3.internal.http.StatusLine.Companion.parse
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Assertions.fail
import org.junit.jupiter.api.Test
class StatusLineTest {
@Test
fun parse() {
val message = "Temporary Redirect"
val version = 1
val code = 200
val statusLine = parse("HTTP/1.$version $code $message")
assertThat(statusLine.message).isEqualTo(message)
assertThat(statusLine.protocol).isEqualTo(Protocol.HTTP_1_1)
assertThat(statusLine.code).isEqualTo(code)
}
@Test
fun emptyMessage() {
val version = 1
val code = 503
val statusLine = parse("HTTP/1.$version $code ")
assertThat(statusLine.message).isEqualTo("")
assertThat(statusLine.protocol).isEqualTo(Protocol.HTTP_1_1)
assertThat(statusLine.code).isEqualTo(code)
}
/**
* This is not defined in the protocol but some servers won't add the leading empty space when the
* message is empty. http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1
*/
@Test
fun emptyMessageAndNoLeadingSpace() {
val version = 1
val code = 503
val statusLine = parse("HTTP/1.$version $code")
assertThat(statusLine.message).isEqualTo("")
assertThat(statusLine.protocol).isEqualTo(Protocol.HTTP_1_1)
assertThat(statusLine.code).isEqualTo(code)
}
// https://github.com/square/okhttp/issues/386
@Test
fun shoutcast() {
val statusLine = parse("ICY 200 OK")
assertThat(statusLine.message).isEqualTo("OK")
assertThat(statusLine.protocol).isEqualTo(Protocol.HTTP_1_0)
assertThat(statusLine.code).isEqualTo(200)
}
@Test
fun missingProtocol() {
assertInvalid("")
assertInvalid(" ")
assertInvalid("200 OK")
assertInvalid(" 200 OK")
}
@Test
fun protocolVersions() {
assertInvalid("HTTP/2.0 200 OK")
assertInvalid("HTTP/2.1 200 OK")
assertInvalid("HTTP/-.1 200 OK")
assertInvalid("HTTP/1.- 200 OK")
assertInvalid("HTTP/0.1 200 OK")
assertInvalid("HTTP/101 200 OK")
assertInvalid("HTTP/1.1_200 OK")
}
@Test
fun nonThreeDigitCode() {
assertInvalid("HTTP/1.1 OK")
assertInvalid("HTTP/1.1 2 OK")
assertInvalid("HTTP/1.1 20 OK")
assertInvalid("HTTP/1.1 2000 OK")
assertInvalid("HTTP/1.1 two OK")
assertInvalid("HTTP/1.1 2")
assertInvalid("HTTP/1.1 2000")
assertInvalid("HTTP/1.1 two")
}
@Test
fun truncated() {
assertInvalid("")
assertInvalid("H")
assertInvalid("HTTP/1")
assertInvalid("HTTP/1.")
assertInvalid("HTTP/1.1")
assertInvalid("HTTP/1.1 ")
assertInvalid("HTTP/1.1 2")
assertInvalid("HTTP/1.1 20")
}
@Test
fun wrongMessageDelimiter() {
assertInvalid("HTTP/1.1 200_")
}
private fun assertInvalid(statusLine: String) {
try {
parse(statusLine)
fail<Any>("")
} catch (expected: ProtocolException) {
}
}
}

View File

@ -1,160 +0,0 @@
/*
* 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.
*/
package okhttp3.internal.http;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import okhttp3.Call;
import okhttp3.DelegatingServerSocketFactory;
import okhttp3.DelegatingSocketFactory;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.OkHttpClientTestRule;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.testing.PlatformRule;
import okio.Buffer;
import okio.BufferedSink;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import static org.junit.jupiter.api.Assertions.fail;
@Tag("Slowish")
public final class ThreadInterruptTest {
@RegisterExtension public final PlatformRule platform = new PlatformRule();
@RegisterExtension public final OkHttpClientTestRule clientTestRule = new OkHttpClientTestRule();
// The size of the socket buffers in bytes.
private static final int SOCKET_BUFFER_SIZE = 256 * 1024;
private MockWebServer server;
private OkHttpClient client;
@BeforeEach public void setUp() throws Exception {
// Sockets on some platforms can have large buffers that mean writes do not block when
// required. These socket factories explicitly set the buffer sizes on sockets created.
server = new MockWebServer();
server.setServerSocketFactory(
new DelegatingServerSocketFactory(ServerSocketFactory.getDefault()) {
@Override
protected ServerSocket configureServerSocket(ServerSocket serverSocket) throws SocketException {
serverSocket.setReceiveBufferSize(SOCKET_BUFFER_SIZE);
return serverSocket;
}
});
client = clientTestRule.newClientBuilder()
.socketFactory(new DelegatingSocketFactory(SocketFactory.getDefault()) {
@Override
protected Socket configureSocket(Socket socket) throws IOException {
socket.setSendBufferSize(SOCKET_BUFFER_SIZE);
socket.setReceiveBufferSize(SOCKET_BUFFER_SIZE);
return socket;
}
})
.build();
}
@AfterEach public void tearDown() throws Exception {
Thread.interrupted(); // Clear interrupted state.
}
@Test public void interruptWritingRequestBody() throws Exception {
server.enqueue(new MockResponse());
server.start();
Call call = client.newCall(new Request.Builder()
.url(server.url("/"))
.post(new RequestBody() {
@Override public @Nullable MediaType contentType() {
return null;
}
@Override public void writeTo(BufferedSink sink) throws IOException {
for (int i = 0; i < 10; i++) {
sink.writeByte(0);
sink.flush();
sleep(100);
}
fail("Expected connection to be closed");
}
})
.build());
interruptLater(500);
try {
call.execute();
fail("");
} catch (IOException expected) {
}
}
@Test public void interruptReadingResponseBody() throws Exception {
int responseBodySize = 8 * 1024 * 1024; // 8 MiB.
server.enqueue(new MockResponse()
.setBody(new Buffer().write(new byte[responseBodySize]))
.throttleBody(64 * 1024, 125, TimeUnit.MILLISECONDS)); // 500 Kbps
server.start();
Call call = client.newCall(new Request.Builder()
.url(server.url("/"))
.build());
Response response = call.execute();
interruptLater(500);
InputStream responseBody = response.body().byteStream();
byte[] buffer = new byte[1024];
try {
while (responseBody.read(buffer) != -1) {
}
fail("Expected connection to be interrupted");
} catch (IOException expected) {
}
responseBody.close();
}
private void sleep(int delayMillis) {
try {
Thread.sleep(delayMillis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void interruptLater(int delayMillis) {
Thread toInterrupt = Thread.currentThread();
Thread interruptingCow = new Thread(() -> {
sleep(delayMillis);
toInterrupt.interrupt();
});
interruptingCow.start();
}
}

View File

@ -0,0 +1,157 @@
/*
* 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.
*/
package okhttp3.internal.http
import java.io.IOException
import java.net.ServerSocket
import java.net.Socket
import java.net.SocketException
import java.util.concurrent.TimeUnit
import okhttp3.DelegatingServerSocketFactory
import okhttp3.DelegatingSocketFactory
import okhttp3.OkHttpClient
import okhttp3.OkHttpClientTestRule
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okhttp3.testing.PlatformRule
import okio.Buffer
import okio.BufferedSink
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.fail
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
@Tag("Slowish")
class ThreadInterruptTest {
@RegisterExtension
val platform = PlatformRule()
@RegisterExtension
val clientTestRule = OkHttpClientTestRule()
private lateinit var server: MockWebServer
private lateinit var client: OkHttpClient
@BeforeEach
fun setUp() {
// Sockets on some platforms can have large buffers that mean writes do not block when
// required. These socket factories explicitly set the buffer sizes on sockets created.
server = MockWebServer()
server.serverSocketFactory = object : DelegatingServerSocketFactory(getDefault()) {
@Throws(SocketException::class)
override fun configureServerSocket(serverSocket: ServerSocket): ServerSocket {
serverSocket.setReceiveBufferSize(SOCKET_BUFFER_SIZE)
return serverSocket
}
}
client = clientTestRule.newClientBuilder()
.socketFactory(object : DelegatingSocketFactory(getDefault()) {
@Throws(IOException::class)
override fun configureSocket(socket: Socket): Socket {
socket.setSendBufferSize(SOCKET_BUFFER_SIZE)
socket.setReceiveBufferSize(SOCKET_BUFFER_SIZE)
return socket
}
})
.build()
}
@AfterEach
fun tearDown() {
Thread.interrupted() // Clear interrupted state.
}
@Test
fun interruptWritingRequestBody() {
server.enqueue(MockResponse())
server.start()
val call = client.newCall(
Request.Builder()
.url(server.url("/"))
.post(object : RequestBody() {
override fun contentType() = null
override fun writeTo(sink: BufferedSink) {
for (i in 0..9) {
sink.writeByte(0)
sink.flush()
sleep(100)
}
fail<Any>("Expected connection to be closed")
}
})
.build()
)
interruptLater(500)
try {
call.execute()
fail<Any>("")
} catch (expected: IOException) {
}
}
@Test
fun interruptReadingResponseBody() {
val responseBodySize = 8 * 1024 * 1024 // 8 MiB.
server.enqueue(
MockResponse()
.setBody(Buffer().write(ByteArray(responseBodySize)))
.throttleBody((64 * 1024).toLong(), 125, TimeUnit.MILLISECONDS)
) // 500 Kbps
server.start()
val call = client.newCall(
Request.Builder()
.url(server.url("/"))
.build()
)
val response = call.execute()
interruptLater(500)
val responseBody = response.body.byteStream()
val buffer = ByteArray(1024)
try {
while (responseBody.read(buffer) != -1) {
}
fail<Any>("Expected connection to be interrupted")
} catch (expected: IOException) {
}
responseBody.close()
}
private fun sleep(delayMillis: Int) {
try {
Thread.sleep(delayMillis.toLong())
} catch (e: InterruptedException) {
Thread.currentThread().interrupt()
}
}
private fun interruptLater(delayMillis: Int) {
val toInterrupt = Thread.currentThread()
val interruptingCow = Thread {
sleep(delayMillis)
toInterrupt.interrupt()
}
interruptingCow.start()
}
companion object {
// The size of the socket buffers in bytes.
private const val SOCKET_BUFFER_SIZE = 256 * 1024
}
}

View File

@ -1,50 +0,0 @@
/*
* Copyright 2013 Twitter, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.internal.http2;
import java.io.IOException;
import java.util.Random;
import okio.Buffer;
import okio.ByteString;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
/** Original version of this class was lifted from {@code com.twitter.hpack.HuffmanTest}. */
public final class HuffmanTest {
@Test public void roundTripForRequestAndResponse() throws IOException {
String s = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (int i = 0; i < s.length(); i++) {
assertRoundTrip(ByteString.encodeUtf8(s.substring(0, i)));
}
Random random = new Random(123456789L);
byte[] buf = new byte[4096];
random.nextBytes(buf);
assertRoundTrip(ByteString.of(buf));
}
private void assertRoundTrip(ByteString data) throws IOException {
Buffer encodeBuffer = new Buffer();
Huffman.INSTANCE.encode(data, encodeBuffer);
assertThat(Huffman.INSTANCE.encodedLength(data)).isEqualTo(encodeBuffer.size());
Buffer decodeBuffer = new Buffer();
Huffman.INSTANCE.decode(encodeBuffer, encodeBuffer.size(), decodeBuffer);
assertEquals(data, decodeBuffer.readByteString());
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright 2013 Twitter, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.internal.http2
import java.util.Random
import okhttp3.internal.http2.Huffman.decode
import okhttp3.internal.http2.Huffman.encode
import okhttp3.internal.http2.Huffman.encodedLength
import okio.Buffer
import okio.ByteString
import okio.ByteString.Companion.encodeUtf8
import okio.ByteString.Companion.toByteString
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
/** Original version of this class was lifted from `com.twitter.hpack.HuffmanTest`. */
class HuffmanTest {
@Test
fun roundTripForRequestAndResponse() {
val s = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
for (i in s.indices) {
assertRoundTrip(s.substring(0, i).encodeUtf8())
}
val random = Random(123456789L)
val buf = ByteArray(4096)
random.nextBytes(buf)
assertRoundTrip(buf.toByteString())
}
private fun assertRoundTrip(data: ByteString) {
val encodeBuffer = Buffer()
encode(data, encodeBuffer)
assertThat(encodedLength(data)).isEqualTo(encodeBuffer.size)
val decodeBuffer = Buffer()
decode(encodeBuffer, encodeBuffer.size, decodeBuffer)
assertEquals(data, decodeBuffer.readByteString())
}
}

View File

@ -1,78 +0,0 @@
/*
* Copyright (C) 2012 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.internal.http2;
import org.junit.jupiter.api.Test;
import static okhttp3.internal.http2.Settings.DEFAULT_INITIAL_WINDOW_SIZE;
import static okhttp3.internal.http2.Settings.MAX_CONCURRENT_STREAMS;
import static org.assertj.core.api.Assertions.assertThat;
public final class SettingsTest {
@Test public void unsetField() {
Settings settings = new Settings();
assertThat(settings.isSet(MAX_CONCURRENT_STREAMS)).isFalse();
assertThat(settings.getMaxConcurrentStreams()).isEqualTo(Integer.MAX_VALUE);
}
@Test public void setFields() {
Settings settings = new Settings();
settings.set(Settings.HEADER_TABLE_SIZE, 8096);
assertThat(settings.getHeaderTableSize()).isEqualTo(8096);
assertThat(settings.getEnablePush(true)).isTrue();
settings.set(Settings.ENABLE_PUSH, 1);
assertThat(settings.getEnablePush(false)).isTrue();
settings.clear();
assertThat(settings.getMaxConcurrentStreams()).isEqualTo(Integer.MAX_VALUE);
settings.set(MAX_CONCURRENT_STREAMS, 75);
assertThat(settings.getMaxConcurrentStreams()).isEqualTo(75);
settings.clear();
assertThat(settings.getMaxFrameSize(16384)).isEqualTo(16384);
settings.set(Settings.MAX_FRAME_SIZE, 16777215);
assertThat(settings.getMaxFrameSize(16384)).isEqualTo(16777215);
assertThat(settings.getMaxHeaderListSize(-1)).isEqualTo(-1);
settings.set(Settings.MAX_HEADER_LIST_SIZE, 16777215);
assertThat(settings.getMaxHeaderListSize(-1)).isEqualTo(16777215);
assertThat(settings.getInitialWindowSize()).isEqualTo(
DEFAULT_INITIAL_WINDOW_SIZE);
settings.set(Settings.INITIAL_WINDOW_SIZE, 108);
assertThat(settings.getInitialWindowSize()).isEqualTo(108);
}
@Test public void merge() {
Settings a = new Settings();
a.set(Settings.HEADER_TABLE_SIZE, 10000);
a.set(Settings.MAX_HEADER_LIST_SIZE, 20000);
a.set(Settings.INITIAL_WINDOW_SIZE, 30000);
Settings b = new Settings();
b.set(Settings.MAX_HEADER_LIST_SIZE, 40000);
b.set(Settings.INITIAL_WINDOW_SIZE, 50000);
b.set(Settings.MAX_CONCURRENT_STREAMS, 60000);
a.merge(b);
assertThat(a.getHeaderTableSize()).isEqualTo(10000);
assertThat(a.getMaxHeaderListSize(-1)).isEqualTo(40000);
assertThat(a.getInitialWindowSize()).isEqualTo(50000);
assertThat(a.getMaxConcurrentStreams()).isEqualTo(60000);
}
}

View File

@ -0,0 +1,71 @@
/*
* Copyright (C) 2012 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.internal.http2
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
class SettingsTest {
@Test
fun unsetField() {
val settings = Settings()
assertThat(settings.isSet(Settings.MAX_CONCURRENT_STREAMS)).isFalse()
assertThat(settings.getMaxConcurrentStreams()).isEqualTo(Int.MAX_VALUE)
}
@Test
fun setFields() {
val settings = Settings()
settings[Settings.HEADER_TABLE_SIZE] = 8096
assertThat(settings.headerTableSize).isEqualTo(8096)
assertThat(settings.getEnablePush(true)).isTrue()
settings[Settings.ENABLE_PUSH] = 1
assertThat(settings.getEnablePush(false)).isTrue()
settings.clear()
assertThat(settings.getMaxConcurrentStreams()).isEqualTo(Int.MAX_VALUE)
settings[Settings.MAX_CONCURRENT_STREAMS] = 75
assertThat(settings.getMaxConcurrentStreams()).isEqualTo(75)
settings.clear()
assertThat(settings.getMaxFrameSize(16384)).isEqualTo(16384)
settings[Settings.MAX_FRAME_SIZE] = 16777215
assertThat(settings.getMaxFrameSize(16384)).isEqualTo(16777215)
assertThat(settings.getMaxHeaderListSize(-1)).isEqualTo(-1)
settings[Settings.MAX_HEADER_LIST_SIZE] = 16777215
assertThat(settings.getMaxHeaderListSize(-1)).isEqualTo(16777215)
assertThat(settings.initialWindowSize).isEqualTo(
Settings.DEFAULT_INITIAL_WINDOW_SIZE
)
settings[Settings.INITIAL_WINDOW_SIZE] = 108
assertThat(settings.initialWindowSize).isEqualTo(108)
}
@Test
fun merge() {
val a = Settings()
a[Settings.HEADER_TABLE_SIZE] = 10000
a[Settings.MAX_HEADER_LIST_SIZE] = 20000
a[Settings.INITIAL_WINDOW_SIZE] = 30000
val b = Settings()
b[Settings.MAX_HEADER_LIST_SIZE] = 40000
b[Settings.INITIAL_WINDOW_SIZE] = 50000
b[Settings.MAX_CONCURRENT_STREAMS] = 60000
a.merge(b)
assertThat(a.headerTableSize).isEqualTo(10000)
assertThat(a.getMaxHeaderListSize(-1)).isEqualTo(40000)
assertThat(a.initialWindowSize).isEqualTo(50000)
assertThat(a.getMaxConcurrentStreams()).isEqualTo(60000)
}
}

View File

@ -1,43 +0,0 @@
/*
* Copyright (C) 2016 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.internal.platform;
import okhttp3.testing.PlatformRule;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import static org.assertj.core.api.Java6Assertions.assertThat;
import static org.junit.jupiter.api.Assumptions.assumeFalse;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
public class Jdk8WithJettyBootPlatformTest {
@RegisterExtension public final PlatformRule platform = new PlatformRule();
@Test
public void testBuildsWithJettyBoot() {
assumeTrue(System.getProperty("java.specification.version").equals("1.8"));
platform.assumeJettyBootEnabled();
assertThat(Jdk8WithJettyBootPlatform.Companion.buildIfSupported()).isNotNull();
}
@Test
public void testNotBuildWithOther() {
assumeFalse(System.getProperty("java.specification.version").equals("1.8"));
assertThat(Jdk8WithJettyBootPlatform.Companion.buildIfSupported()).isNull();
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright (C) 2016 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.internal.platform
import okhttp3.internal.platform.Jdk8WithJettyBootPlatform.Companion.buildIfSupported
import okhttp3.testing.PlatformRule
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Assumptions.assumeFalse
import org.junit.jupiter.api.Assumptions.assumeTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
class Jdk8WithJettyBootPlatformTest {
@RegisterExtension
val platform = PlatformRule()
@Test
fun testBuildsWithJettyBoot() {
assumeTrue(System.getProperty("java.specification.version") == "1.8")
platform.assumeJettyBootEnabled()
assertThat(buildIfSupported()).isNotNull()
}
@Test
fun testNotBuildWithOther() {
assumeFalse(System.getProperty("java.specification.version") == "1.8")
assertThat(buildIfSupported()).isNull()
}
}

View File

@ -1,66 +0,0 @@
/*
* Copyright (C) 2016 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.internal.platform;
import javax.net.ssl.SSLSocket;
import okhttp3.DelegatingSSLSocket;
import okhttp3.testing.PlatformRule;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import static org.assertj.core.api.Assertions.assertThat;
public class Jdk9PlatformTest {
@RegisterExtension public final PlatformRule platform = new PlatformRule();
@Test
public void buildsWhenJdk9() {
platform.assumeJdk9();
assertThat(Jdk9Platform.Companion.buildIfSupported()).isNotNull();
}
@Test
public void buildsWhenJdk8() {
platform.assumeJdk8();
try {
SSLSocket.class.getMethod("getApplicationProtocol");
// also present on JDK8 after build 252.
assertThat(Jdk9Platform.Companion.buildIfSupported()).isNotNull();
} catch (NoSuchMethodException nsme) {
assertThat(Jdk9Platform.Companion.buildIfSupported()).isNull();
}
}
@Test
public void testToStringIsClassname() {
assertThat(new Jdk9Platform().toString()).isEqualTo("Jdk9Platform");
}
@Test
public void selectedProtocolIsNullWhenSslSocketThrowsExceptionForApplicationProtocol() {
platform.assumeJdk9();
DelegatingSSLSocket applicationProtocolUnsupported = new DelegatingSSLSocket(null) {
@Override public String getApplicationProtocol() {
throw new UnsupportedOperationException("Mock exception");
}
};
assertThat(new Jdk9Platform().getSelectedProtocol(applicationProtocolUnsupported)).isNull();
}
}

View File

@ -0,0 +1,63 @@
/*
* Copyright (C) 2016 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.internal.platform
import javax.net.ssl.SSLSocket
import okhttp3.DelegatingSSLSocket
import okhttp3.internal.platform.Jdk9Platform.Companion.buildIfSupported
import okhttp3.testing.PlatformRule
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
class Jdk9PlatformTest {
@RegisterExtension
val platform = PlatformRule()
@Test
fun buildsWhenJdk9() {
platform.assumeJdk9()
assertThat(buildIfSupported()).isNotNull()
}
@Test
fun buildsWhenJdk8() {
platform.assumeJdk8()
try {
SSLSocket::class.java.getMethod("getApplicationProtocol")
// also present on JDK8 after build 252.
assertThat(buildIfSupported()).isNotNull()
} catch (nsme: NoSuchMethodException) {
assertThat(buildIfSupported()).isNull()
}
}
@Test
fun testToStringIsClassname() {
assertThat(Jdk9Platform().toString()).isEqualTo("Jdk9Platform")
}
@Test
fun selectedProtocolIsNullWhenSslSocketThrowsExceptionForApplicationProtocol() {
platform.assumeJdk9()
val applicationProtocolUnsupported = object : DelegatingSSLSocket(null) {
override fun getApplicationProtocol(): String {
throw UnsupportedOperationException("Mock exception")
}
}
assertThat(Jdk9Platform().getSelectedProtocol(applicationProtocolUnsupported)).isNull()
}
}

View File

@ -1,52 +0,0 @@
/*
* Copyright (C) 2016 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.internal.platform;
import okhttp3.testing.PlatformRule;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import static org.assertj.core.api.Assertions.assertThat;
public class PlatformTest {
@RegisterExtension public PlatformRule platform = new PlatformRule();
@Test public void alwaysBuilds() {
new Platform();
}
/** Guard against the default value changing by accident. */
@Test public void defaultPrefix() {
assertThat(new Platform().getPrefix()).isEqualTo("OkHttp");
}
public static String getJvmSpecVersion() {
return System.getProperty("java.specification.version", "unknown");
}
@Test
public void testToStringIsClassname() {
assertThat(new Platform().toString()).isEqualTo("Platform");
}
@Test
public void testNotAndroid() {
platform.assumeNotAndroid();
// This is tautological so just confirms that it runs.
assertThat(Platform.Companion.isAndroid()).isEqualTo(false);
}
}

View File

@ -0,0 +1,51 @@
/*
* Copyright (C) 2016 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.internal.platform
import okhttp3.internal.platform.Platform.Companion.isAndroid
import okhttp3.testing.PlatformRule
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
class PlatformTest {
@RegisterExtension
var platform = PlatformRule()
@Test
fun alwaysBuilds() {
Platform()
}
/** Guard against the default value changing by accident. */
@Test
fun defaultPrefix() {
assertThat(Platform().getPrefix()).isEqualTo("OkHttp")
}
@Test
fun testToStringIsClassname() {
assertThat(Platform().toString()).isEqualTo("Platform")
}
@Test
fun testNotAndroid() {
platform.assumeNotAndroid()
// This is tautological so just confirms that it runs.
assertThat(isAndroid).isEqualTo(false)
}
}

View File

@ -1,401 +0,0 @@
/*
* Copyright (C) 2016 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.internal.ws;
import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okhttp3.internal.platform.Platform;
import okio.ByteString;
import static org.assertj.core.api.Assertions.assertThat;
public final class WebSocketRecorder extends WebSocketListener {
private final String name;
private final BlockingQueue<Object> events = new LinkedBlockingQueue<>();
private WebSocketListener delegate;
public WebSocketRecorder(String name) {
this.name = name;
}
/** Sets a delegate for handling the next callback to this listener. Cleared after invoked. */
public void setNextEventDelegate(WebSocketListener delegate) {
this.delegate = delegate;
}
@Override public void onOpen(WebSocket webSocket, Response response) {
Platform.get().log("[WS " + name + "] onOpen", Platform.INFO, null);
WebSocketListener delegate = this.delegate;
if (delegate != null) {
this.delegate = null;
delegate.onOpen(webSocket, response);
} else {
events.add(new Open(webSocket, response));
}
}
@Override public void onMessage(WebSocket webSocket, ByteString bytes) {
Platform.get().log("[WS " + name + "] onMessage", Platform.INFO, null);
WebSocketListener delegate = this.delegate;
if (delegate != null) {
this.delegate = null;
delegate.onMessage(webSocket, bytes);
} else {
Message event = new Message(bytes);
events.add(event);
}
}
@Override public void onMessage(WebSocket webSocket, String text) {
Platform.get().log("[WS " + name + "] onMessage", Platform.INFO, null);
WebSocketListener delegate = this.delegate;
if (delegate != null) {
this.delegate = null;
delegate.onMessage(webSocket, text);
} else {
Message event = new Message(text);
events.add(event);
}
}
@Override public void onClosing(WebSocket webSocket, int code, String reason) {
Platform.get().log("[WS " + name + "] onClosing " + code, Platform.INFO, null);
WebSocketListener delegate = this.delegate;
if (delegate != null) {
this.delegate = null;
delegate.onClosing(webSocket, code, reason);
} else {
events.add(new Closing(code, reason));
}
}
@Override public void onClosed(WebSocket webSocket, int code, String reason) {
Platform.get().log("[WS " + name + "] onClosed " + code, Platform.INFO, null);
WebSocketListener delegate = this.delegate;
if (delegate != null) {
this.delegate = null;
delegate.onClosed(webSocket, code, reason);
} else {
events.add(new Closed(code, reason));
}
}
@Override public void onFailure(WebSocket webSocket, Throwable t, @Nullable Response response) {
Platform.get().log("[WS " + name + "] onFailure", Platform.INFO, t);
WebSocketListener delegate = this.delegate;
if (delegate != null) {
this.delegate = null;
delegate.onFailure(webSocket, t, response);
} else {
events.add(new Failure(t, response));
}
}
private Object nextEvent() {
try {
Object event = events.poll(10, TimeUnit.SECONDS);
if (event == null) {
throw new AssertionError("Timed out waiting for event.");
}
return event;
} catch (InterruptedException e) {
throw new AssertionError(e);
}
}
public void assertTextMessage(String payload) {
Object actual = nextEvent();
assertThat(actual).isEqualTo(new Message(payload));
}
public void assertBinaryMessage(ByteString payload) {
Object actual = nextEvent();
assertThat(actual).isEqualTo(new Message(payload));
}
public void assertPing(ByteString payload) {
Object actual = nextEvent();
assertThat(actual).isEqualTo(new Ping(payload));
}
public void assertPong(ByteString payload) {
Object actual = nextEvent();
assertThat(actual).isEqualTo(new Pong(payload));
}
public void assertClosing(int code, String reason) {
Object actual = nextEvent();
assertThat(actual).isEqualTo(new Closing(code, reason));
}
public void assertClosed(int code, String reason) {
Object actual = nextEvent();
assertThat(actual).isEqualTo(new Closed(code, reason));
}
public void assertExhausted() {
assertThat(events).isEmpty();
}
public WebSocket assertOpen() {
Object event = nextEvent();
if (!(event instanceof Open)) {
throw new AssertionError("Expected Open but was " + event);
}
return ((Open) event).webSocket;
}
public void assertFailure(Throwable t) {
Object event = nextEvent();
if (!(event instanceof Failure)) {
throw new AssertionError("Expected Failure but was " + event);
}
Failure failure = (Failure) event;
assertThat(failure.response).isNull();
assertThat(failure.t).isSameAs(t);
}
public void assertFailure(Class<? extends IOException> cls, String... messages) {
Object event = nextEvent();
if (!(event instanceof Failure)) {
throw new AssertionError("Expected Failure but was " + event);
}
Failure failure = (Failure) event;
assertThat(failure.response).isNull();
assertThat(failure.t.getClass()).isEqualTo(cls);
if (messages.length > 0) {
assertThat(messages).contains(failure.t.getMessage());
}
}
public void assertFailure() {
Object event = nextEvent();
if (!(event instanceof Failure)) {
throw new AssertionError("Expected Failure but was " + event);
}
}
public void assertFailure(int code, String body, Class<? extends IOException> cls, String message)
throws IOException {
Object event = nextEvent();
if (!(event instanceof Failure)) {
throw new AssertionError("Expected Failure but was " + event);
}
Failure failure = (Failure) event;
assertThat(failure.response.code()).isEqualTo(code);
if (body != null) {
assertThat(failure.responseBody).isEqualTo(body);
}
assertThat(failure.t.getClass()).isEqualTo(cls);
assertThat(failure.t.getMessage()).isEqualTo(message);
}
/** Expose this recorder as a frame callback and shim in "ping" events. */
public WebSocketReader.FrameCallback asFrameCallback() {
return new WebSocketReader.FrameCallback() {
@Override public void onReadMessage(String text) throws IOException {
onMessage(null, text);
}
@Override public void onReadMessage(ByteString bytes) throws IOException {
onMessage(null, bytes);
}
@Override public void onReadPing(ByteString payload) {
events.add(new Ping(payload));
}
@Override public void onReadPong(ByteString payload) {
events.add(new Pong(payload));
}
@Override public void onReadClose(int code, String reason) {
onClosing(null, code, reason);
}
};
}
static final class Open {
final WebSocket webSocket;
final Response response;
Open(WebSocket webSocket, Response response) {
this.webSocket = webSocket;
this.response = response;
}
@Override public String toString() {
return "Open[" + response + "]";
}
}
static final class Failure {
final Throwable t;
final Response response;
final String responseBody;
Failure(Throwable t, Response response) {
this.t = t;
this.response = response;
String responseBody = null;
if (response != null && response.code() != 101) {
try {
responseBody = response.body().string();
} catch (IOException ignored) {
}
}
this.responseBody = responseBody;
}
@Override public String toString() {
if (response == null) {
return "Failure[" + t + "]";
}
return "Failure[" + response + "]";
}
}
static final class Message {
public final ByteString bytes;
public final String string;
public Message(ByteString bytes) {
this.bytes = bytes;
this.string = null;
}
public Message(String string) {
this.bytes = null;
this.string = string;
}
@Override public String toString() {
return "Message[" + (bytes != null ? bytes : string) + "]";
}
@Override public int hashCode() {
return (bytes != null ? bytes : string).hashCode();
}
@Override public boolean equals(Object other) {
return other instanceof Message
&& Objects.equals(((Message) other).bytes, bytes)
&& Objects.equals(((Message) other).string, string);
}
}
static final class Ping {
public final ByteString payload;
public Ping(ByteString payload) {
this.payload = payload;
}
@Override public String toString() {
return "Ping[" + payload + "]";
}
@Override public int hashCode() {
return payload.hashCode();
}
@Override public boolean equals(Object other) {
return other instanceof Ping
&& ((Ping) other).payload.equals(payload);
}
}
static final class Pong {
public final ByteString payload;
public Pong(ByteString payload) {
this.payload = payload;
}
@Override public String toString() {
return "Pong[" + payload + "]";
}
@Override public int hashCode() {
return payload.hashCode();
}
@Override public boolean equals(Object other) {
return other instanceof Pong
&& ((Pong) other).payload.equals(payload);
}
}
static final class Closing {
public final int code;
public final String reason;
Closing(int code, String reason) {
this.code = code;
this.reason = reason;
}
@Override public String toString() {
return "Closing[" + code + " " + reason + "]";
}
@Override public int hashCode() {
return code * 37 + reason.hashCode();
}
@Override public boolean equals(Object other) {
return other instanceof Closing
&& ((Closing) other).code == code
&& ((Closing) other).reason.equals(reason);
}
}
static final class Closed {
public final int code;
public final String reason;
Closed(int code, String reason) {
this.code = code;
this.reason = reason;
}
@Override public String toString() {
return "Closed[" + code + " " + reason + "]";
}
@Override public int hashCode() {
return code * 37 + reason.hashCode();
}
@Override public boolean equals(Object other) {
return other instanceof Closed
&& ((Closed) other).code == code
&& ((Closed) other).reason.equals(reason);
}
}
}

View File

@ -0,0 +1,239 @@
/*
* Copyright (C) 2016 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.internal.ws
import java.io.IOException
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.TimeUnit
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import okhttp3.internal.platform.Platform
import okio.ByteString
import org.assertj.core.api.Assertions.assertThat
class WebSocketRecorder(
private val name: String,
) : WebSocketListener() {
private val events = LinkedBlockingQueue<Any>()
private var delegate: WebSocketListener? = null
/** Sets a delegate for handling the next callback to this listener. Cleared after invoked. */
fun setNextEventDelegate(delegate: WebSocketListener?) {
this.delegate = delegate
}
override fun onOpen(webSocket: WebSocket, response: Response) {
Platform.get().log("[WS $name] onOpen", Platform.INFO, null)
val delegate = delegate
if (delegate != null) {
this.delegate = null
delegate.onOpen(webSocket, response)
} else {
events.add(Open(webSocket, response))
}
}
override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
Platform.get().log("[WS $name] onMessage", Platform.INFO, null)
val delegate = delegate
if (delegate != null) {
this.delegate = null
delegate.onMessage(webSocket, bytes)
} else {
events.add(Message(bytes = bytes))
}
}
override fun onMessage(webSocket: WebSocket, text: String) {
Platform.get().log("[WS $name] onMessage", Platform.INFO, null)
val delegate = delegate
if (delegate != null) {
this.delegate = null
delegate.onMessage(webSocket, text)
} else {
events.add(Message(string = text))
}
}
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
Platform.get().log("[WS $name] onClosing $code", Platform.INFO, null)
val delegate = delegate
if (delegate != null) {
this.delegate = null
delegate.onClosing(webSocket, code, reason)
} else {
events.add(Closing(code, reason))
}
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
Platform.get().log("[WS $name] onClosed $code", Platform.INFO, null)
val delegate = delegate
if (delegate != null) {
this.delegate = null
delegate.onClosed(webSocket, code, reason)
} else {
events.add(Closed(code, reason))
}
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
Platform.get().log("[WS $name] onFailure", Platform.INFO, t)
val delegate = delegate
if (delegate != null) {
this.delegate = null
delegate.onFailure(webSocket, t, response)
} else {
events.add(Failure(t, response))
}
}
private fun nextEvent(): Any {
return events.poll(10, TimeUnit.SECONDS)
?: throw AssertionError("Timed out waiting for event.")
}
fun assertTextMessage(payload: String?) {
assertThat(nextEvent()).isEqualTo(Message(string = payload))
}
fun assertBinaryMessage(payload: ByteString?) {
assertThat(nextEvent()).isEqualTo(Message(payload))
}
fun assertPing(payload: ByteString) {
assertThat(nextEvent()).isEqualTo(Ping(payload))
}
fun assertPong(payload: ByteString) {
assertThat(nextEvent()).isEqualTo(Pong(payload))
}
fun assertClosing(code: Int, reason: String) {
assertThat(nextEvent()).isEqualTo(Closing(code, reason))
}
fun assertClosed(code: Int, reason: String) {
assertThat(nextEvent()).isEqualTo(Closed(code, reason))
}
fun assertExhausted() {
assertThat(events).isEmpty()
}
fun assertOpen(): WebSocket {
val event = nextEvent() as Open
return event.webSocket
}
fun assertFailure(t: Throwable?) {
val event = nextEvent() as Failure
assertThat(event.response).isNull()
assertThat(event.t).isSameAs(t)
}
fun assertFailure(cls: Class<out IOException?>?, vararg messages: String) {
val event = nextEvent() as Failure
assertThat(event.response).isNull()
assertThat(event.t.javaClass).isEqualTo(cls)
if (messages.isNotEmpty()) {
assertThat(messages).contains(event.t.message)
}
}
fun assertFailure() {
nextEvent() as Failure
}
fun assertFailure(code: Int, body: String?, cls: Class<out IOException?>?, message: String?) {
val event = nextEvent() as Failure
assertThat(event.response!!.code).isEqualTo(code)
if (body != null) {
assertThat(event.responseBody).isEqualTo(body)
}
assertThat(event.t.javaClass).isEqualTo(cls)
assertThat(event.t.message).isEqualTo(message)
}
/** Expose this recorder as a frame callback and shim in "ping" events. */
fun asFrameCallback() = object : WebSocketReader.FrameCallback {
override fun onReadMessage(text: String) {
events.add(Message(string = text))
}
override fun onReadMessage(bytes: ByteString) {
events.add(Message(bytes = bytes))
}
override fun onReadPing(payload: ByteString) {
events.add(Ping(payload))
}
override fun onReadPong(payload: ByteString) {
events.add(Pong(payload))
}
override fun onReadClose(code: Int, reason: String) {
events.add(Closing(code, reason))
}
}
internal class Open(
val webSocket: WebSocket,
val response: Response,
)
internal class Failure(
val t: Throwable,
val response: Response?,
) {
val responseBody: String? = when {
response != null && response.code != 101 -> response.body.string()
else -> null
}
override fun toString(): String {
return when (response) {
null -> "Failure[$t]"
else -> "Failure[$response]"
}
}
}
internal data class Message(
val bytes: ByteString? = null,
val string: String? = null,
)
internal data class Ping(
val payload: ByteString,
)
internal data class Pong(
val payload: ByteString,
)
internal data class Closing(
val code: Int,
val reason: String,
)
internal data class Closed(
val code: Int,
val reason: String,
)
}

View File

@ -1,161 +0,0 @@
/*
* Copyright (C) 2020 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.osgi;
import aQute.bnd.build.Project;
import aQute.bnd.build.Workspace;
import aQute.bnd.build.model.BndEditModel;
import aQute.bnd.deployer.repository.LocalIndexedRepo;
import aQute.bnd.osgi.Constants;
import aQute.bnd.service.RepositoryPlugin;
import biz.aQute.resolve.Bndrun;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import okio.BufferedSource;
import okio.Okio;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@Tag("Slow")
public final class OsgiTest {
/** Each is the Bundle-SymbolicName of an OkHttp module's OSGi configuration. */
private static final List<String> REQUIRED_BUNDLES = Arrays.asList(
"com.squareup.okhttp3",
"com.squareup.okhttp3.brotli",
"com.squareup.okhttp3.dnsoverhttps",
"com.squareup.okhttp3.logging",
"com.squareup.okhttp3.sse",
"com.squareup.okhttp3.tls",
"com.squareup.okhttp3.urlconnection"
);
/** Equinox must also be on the testing classpath. */
private static final String RESOLVE_OSGI_FRAMEWORK = "org.eclipse.osgi";
private static final String RESOLVE_JAVA_VERSION = "JavaSE-1.8";
private static final String REPO_NAME = "OsgiTest";
private File testResourceDir;
private File workspaceDir;
@BeforeEach
public void setUp() throws Exception {
testResourceDir = new File("./build/resources/test/okhttp3/osgi");
workspaceDir = new File(testResourceDir, "workspace");
// Ensure we start from scratch.
deleteDirectory(workspaceDir);
workspaceDir.mkdirs();
}
/**
* Resolve the OSGi metadata of the all okhttp3 modules. If required modules do not have OSGi
* metadata this will fail with an exception.
*/
@Test
public void testMainModuleWithSiblings() throws Exception {
try (Workspace workspace = createWorkspace();
Bndrun bndRun = createBndRun(workspace)) {
bndRun.resolve(false, false);
}
}
private Workspace createWorkspace() throws Exception {
File bndDir = new File(workspaceDir, "cnf");
File repoDir = new File(bndDir, "repo");
repoDir.mkdirs();
Workspace workspace = new Workspace(workspaceDir, bndDir.getName());
workspace.setProperty(Constants.PLUGIN + "." + REPO_NAME, ""
+ LocalIndexedRepo.class.getName()
+ "; " + LocalIndexedRepo.PROP_NAME + " = '" + REPO_NAME + "'"
+ "; " + LocalIndexedRepo.PROP_LOCAL_DIR + " = '" + repoDir + "'");
workspace.refresh();
prepareWorkspace(workspace);
return workspace;
}
private void prepareWorkspace(Workspace workspace) throws Exception {
RepositoryPlugin repositoryPlugin = workspace.getRepository(REPO_NAME);
// Deploy the bundles in the deployments test directory.
deployDirectory(repositoryPlugin, new File(testResourceDir, "deployments"));
deployClassPath(repositoryPlugin);
}
private Bndrun createBndRun(Workspace workspace) throws Exception {
// Creating the run require string. It will always use the latest version of each bundle
// available in the repository.
String runRequireString = REQUIRED_BUNDLES.stream()
.map(s -> "osgi.identity;filter:='(osgi.identity=" + s + ")'")
.collect(Collectors.joining(","));
BndEditModel bndEditModel = new BndEditModel(workspace);
// Temporary project to satisfy bnd API.
bndEditModel.setProject(new Project(workspace, workspaceDir));
Bndrun result = new Bndrun(bndEditModel);
result.setRunfw(RESOLVE_OSGI_FRAMEWORK);
result.setRunee(RESOLVE_JAVA_VERSION);
result.setRunRequires(runRequireString);
return result;
}
private void deployDirectory(RepositoryPlugin repository, File directory) throws Exception {
File[] files = directory.listFiles();
if (files == null) return;
for (File file : files) {
deployFile(repository, file);
}
}
private void deployClassPath(RepositoryPlugin repositoryPlugin) throws Exception {
String classpath = System.getProperty("java.class.path");
for (String classPathEntry : classpath.split(File.pathSeparator)) {
deployFile(repositoryPlugin, new File(classPathEntry));
}
}
private void deployFile(RepositoryPlugin repositoryPlugin, File file) throws Exception {
if (!file.exists() || file.isDirectory()) return;
try (BufferedSource source = Okio.buffer(Okio.source(file))) {
repositoryPlugin.put(source.inputStream(), new RepositoryPlugin.PutOptions());
System.out.println("Deployed " + file.getName());
} catch (IllegalArgumentException e) {
if (e.getMessage().contains("Jar does not have a symbolic name")) {
System.out.println("Skipped non-OSGi dependency: " + file.getName());
return;
}
throw e;
}
}
private static void deleteDirectory(File dir) throws IOException {
if (!dir.exists()) return;
Files.walk(dir.toPath())
.filter(Files::isRegularFile)
.map(Path::toFile)
.forEach(File::delete);
}
}

View File

@ -0,0 +1,160 @@
/*
* Copyright (C) 2020 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.osgi
import aQute.bnd.build.Project
import aQute.bnd.build.Workspace
import aQute.bnd.build.model.BndEditModel
import aQute.bnd.deployer.repository.LocalIndexedRepo
import aQute.bnd.osgi.Constants
import aQute.bnd.service.RepositoryPlugin
import biz.aQute.resolve.Bndrun
import java.io.File
import okio.FileSystem
import okio.Path
import okio.Path.Companion.toPath
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
@Tag("Slow")
class OsgiTest {
private lateinit var testResourceDir: Path
private lateinit var workspaceDir: Path
@BeforeEach
fun setUp() {
testResourceDir = "./build/resources/test/okhttp3/osgi".toPath()
workspaceDir = testResourceDir / "workspace"
// Ensure we start from scratch.
fileSystem.deleteRecursively(workspaceDir)
fileSystem.createDirectories(workspaceDir)
}
/**
* Resolve the OSGi metadata of the all okhttp3 modules. If required modules do not have OSGi
* metadata this will fail with an exception.
*/
@Test
fun testMainModuleWithSiblings() {
createWorkspace().use { workspace ->
createBndRun(workspace).use { bndRun ->
bndRun.resolve(
false,
false
)
}
}
}
private fun createWorkspace(): Workspace {
val bndDir = workspaceDir / "cnf"
val repoDir = bndDir / "repo"
fileSystem.createDirectories(repoDir)
return Workspace(workspaceDir.toFile(), bndDir.name)
.apply {
setProperty(
"${Constants.PLUGIN}.$REPO_NAME",
LocalIndexedRepo::class.java.getName() +
"; ${LocalIndexedRepo.PROP_NAME} = '$REPO_NAME'" +
"; ${LocalIndexedRepo.PROP_LOCAL_DIR} = '$repoDir'"
)
refresh()
prepareWorkspace()
}
}
private fun Workspace.prepareWorkspace() {
val repositoryPlugin = getRepository(REPO_NAME)
// Deploy the bundles in the deployments test directory.
repositoryPlugin.deployDirectory(testResourceDir / "deployments")
repositoryPlugin.deployClassPath()
}
private fun createBndRun(workspace: Workspace): Bndrun {
// Creating the run require string. It will always use the latest version of each bundle
// available in the repository.
val runRequireString = REQUIRED_BUNDLES.joinToString(separator = ",") {
"osgi.identity;filter:='(osgi.identity=$it)'"
}
val bndEditModel = BndEditModel(workspace).apply {
// Temporary project to satisfy bnd API.
project = Project(workspace, workspaceDir.toFile())
}
return Bndrun(bndEditModel).apply {
setRunfw(RESOLVE_OSGI_FRAMEWORK)
runee = RESOLVE_JAVA_VERSION
setRunRequires(runRequireString)
}
}
private fun RepositoryPlugin.deployDirectory(directory: Path) {
for (path in fileSystem.list(directory)) {
deployFile(path)
}
}
private fun RepositoryPlugin.deployClassPath() {
val classpath = System.getProperty("java.class.path")
val entries = classpath.split(File.pathSeparator.toRegex())
.dropLastWhile { it.isEmpty() }
.toTypedArray()
for (classPathEntry in entries) {
deployFile(classPathEntry.toPath())
}
}
private fun RepositoryPlugin.deployFile(file: Path) {
if (fileSystem.metadataOrNull(file)?.isRegularFile != true) return
try {
fileSystem.read(file) {
put(inputStream(), RepositoryPlugin.PutOptions())
println("Deployed ${file.name}")
}
} catch (e: IllegalArgumentException) {
if ("Jar does not have a symbolic name" in e.message!!) {
println("Skipped non-OSGi dependency: ${file.name}")
return
}
throw e
}
}
companion object {
val fileSystem = FileSystem.SYSTEM
/** Each is the Bundle-SymbolicName of an OkHttp module's OSGi configuration. */
private val REQUIRED_BUNDLES: List<String> = mutableListOf(
"com.squareup.okhttp3",
"com.squareup.okhttp3.brotli",
"com.squareup.okhttp3.dnsoverhttps",
"com.squareup.okhttp3.logging",
"com.squareup.okhttp3.sse",
"com.squareup.okhttp3.tls",
"com.squareup.okhttp3.urlconnection"
)
/** Equinox must also be on the testing classpath. */
private const val RESOLVE_OSGI_FRAMEWORK = "org.eclipse.osgi"
private const val RESOLVE_JAVA_VERSION = "JavaSE-1.8"
private const val REPO_NAME = "OsgiTest"
}
}